Repository navigation
Expand file tree
/
Copy pathsort.cpp
More file actions
2153 lines (1984 loc) · 77.9 KB
/
Copy pathsort.cpp
File metadata and controls
2153 lines (1984 loc) · 77.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ============================================================
// 插入排序(Insertion Sort)
// ============================================================
//
// 【核心思想】
// 将数组分为"已排序"和"未排序"两部分。每次从未排序部分取第一个
// 元素,在已排序部分中找到它的正确位置并插入。就像打牌时整理
// 手牌——拿到新牌后,从右向左找它应该在的位置,然后插进去。
//
// 【算法步骤】
// 1. 从第 2 个元素(索引 1)开始,认为索引 0 已经有序
// 2. 取出当前元素 key = arr[i]
// 3. 从 i-1 向左扫描,将比 key 大的元素都右移一格
// 4. 将 key 放入空出的位置
//
// 【时间复杂度】
// 最好(已有序): O(n) —— 内层 while 从不执行
// 最坏(逆序): O(n²) —— 每次都要移到最前面
// 平均: O(n²)
//
// 【稳定性】稳定 —— 相等元素不会交换位置
//
// 【适用场景】
// - 数据量小(n < 50)时比快排/归并更快(常数因子小)
// - 数据基本有序时几乎是 O(n)
// - 实际中常用于混合排序(如 TimSort 对小数组用插入排序)
//
// 【与冒泡/选择排序的区别】
// 冒泡:两两比较交换,每轮把最大元素"浮"到最后
// 选择:每轮选最小元素放到前面
// 插入:把当前元素插入前面已排序部分的正确位置
// 三者都是 O(n²),但插入排序通常更快(交换次数更少)
// ============================================================
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#include<queue>
using namespace std;
// 【阅读地图】
// 不要按文件顺序背诵 20 多种排序。建议按“局部有序、分治、堆、键域、外部排序”分组比较。
// 阅读每个算法时都记录四件事:循环不变量、稳定性、额外空间、触发最坏情况的输入。
// 稳定性只在相等键的相对次序上定义;快速、堆和通常的选择排序不稳定。
void insertion_sort(vector<int>& arr)
{
int n = arr.size();
// 从第二个元素开始,左边 arr[0..i-1] 已有序,右边 arr[i..n-1] 待处理
for (int i = 1; i < n; i++)
{
int key = arr[i]; // 当前待插入的元素("新牌")
int j = i - 1;
// 从右向左扫描已排序部分,将比 key 大的元素逐个右移
while (j >= 0 && arr[j] > key)
{
arr[j + 1] = arr[j]; // 右移,给 key 腾位置
j--;
}
arr[j + 1] = key; // 插入到正确位置
}
}
// ============================================================
// 原始冒泡排序(Bubble Sort)—— 升序
// ============================================================
//
// 【核心思想】
// 每轮从左到右比较相邻元素,如果"前 > 后"就交换,
// 一轮结束后最大元素就像气泡一样"浮"到了最右端。
// n-1 轮后数组升序排列。
//
// 【痛点】
// 即使数组已经有序,原始冒泡仍然傻傻跑完所有 n(n-1)/2 次比较。
// 最好情况也是 O(n²),毫无"感知有序"的能力。
//
// ============================================================
void bubble_sort(vector<int>& arr)
{
int n = arr.size();
for (int i = 0; i < n; i++)
{
// 每轮最后 i 个元素已经排好,缩小右边界
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1]) // 前 > 后 → 交换(升序)
{
swap(arr[j], arr[j + 1]);
}
}
}
}
// ============================================================
// 方案一:提前停机 —— 布尔标志位(Flagged Bubble Sort)
// ============================================================
//
// 【优化思路】
// 如果某轮遍历中没有任何元素交换位置,说明数组已经完全有序。
// 此时直接 break 退出所有循环,后面的轮次全都可以省掉。
// 就像排队时老师看了一眼说"已经整齐了",就不需要重新排了。
//
// 【物理直觉】
// 问一句"这一趟有没有人换过座位?"
// 没有 → 已经有序 → 收工!
//
// 【效果】
// 最好情况(已有序):第 1 轮跑一遍发现无需交换 → 直接退出 → O(n)
// 最坏情况(完全逆序):仍然 O(n²),但每轮都要交换到底
//
// 【实现细节】
// swapped 在每轮开始时设为 false,发生交换时置为 true,
// 轮末检查 swapped,若为 false 则提前终止。
//
// ============================================================
void bubble_sort_v1_flagged(vector<int>& arr)
{
int n = arr.size();
for (int i = 0; i < n; i++)
{
bool swapped = false; // 本轮是否发生过交换
// ↑ 每轮开始时,假设"已经有序"
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
swapped = true; // 发生了交换 → 说明还不完全有序
}
}
// 如果整轮都没有交换 → 已完全有序,后面的轮次直接跳过
if (!swapped)
{
break;
}
}
}
// ============================================================
// 方案二:缩小边界 —— 记录最后一次交换位置
// (Right-Bound Shrinking Bubble Sort)
// ============================================================
//
// 【优化思路】
// 方案一的 swapped 只回答"有没有交换"(是/否),但更精细的问题是:
// "最后一次交换发生在哪个位置?"
//
// 这个位置之后的所有元素,必然已经有序!因为它们之间再也没
// 发生过交换——说明从该位置到数组末尾,所有相邻元素都已满足
// 前 ≤ 后的升序关系。
//
// 因此,下一次循环的右边界可以直接收缩到"最后一次交换的位置",
// 而非固定的 n - i - 1。
//
// 【物理直觉】
// 在一次巡视中,最后一次发生交换的位置就是"混乱区"与"有序区"
// 的分界线。边界右侧 → 绝对有序,以后都不用看了。
//
// 【效果】
// 当数组后半部分已经有序时,大幅减少无效比较次数。
// 例如:[3,1,4,5,6,7,8] → 第一次遍历到最后交换位置 = 1,
// 下一轮右边界直接缩到 1,后面 [4,5,6,7,8] 全部跳过。
//
// 【与方案一的区别】
// 方案一:只能判断"全有序 / 不全有序"
// 方案二:能精确定位"有序开始的位置"
// 方案二包含方案一的逻辑——若 lastSwapPos = 0(没交换过),自动退出
//
// ============================================================
void bubble_sort_v2_bound(vector<int>& arr)
{
int n = arr.size();
int rightBound = n - 1; // 初始右边界 = 数组末尾
for (int i = 0; i < n; i++)
{
int lastSwapPos = 0; // 本轮最后一次交换的位置
// ↑ 初始化为 0,若保持 0 说明没交换过
for (int j = 0; j < rightBound; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
lastSwapPos = j; // 记录最后一次交换的位置
// ↑ 这个位置之后已经有序了!
}
}
// 下一轮的右边界直接收缩到最后发生交换的位置
rightBound = lastSwapPos;
// 如果右边界为 0 → 没有发生任何交换 → 全部有序
if (rightBound == 0)
{
break;
}
}
}
// ============================================================
// 方案三:双向巡视 —— 鸡尾酒排序
// (Cocktail Shaker Sort / Bidirectional Bubble Sort)
// ============================================================
//
// 【优化思路】
// 传统冒泡有一个严重的不对称问题——"兔子与乌龟":
//
// - 大元素(兔子):向右移动极快,一趟就能从最左跑到最右
// - 小元素(乌龟):向左移动极慢,每次只能挪 1 格!
//
// 例如 [9,2,3,4,5,6,1] 升序排列:
// 第一趟从左→右:9 像兔子一样跳到最右边 → [2,3,4,5,6,1,9]
// 第二趟从左→右:6 跳到 1 右边 → [2,3,4,5,1,6,9]
// ... 可怜的 1 每次只能向左爬 1 格,需要 n-2 趟才能到最左!
//
// 解决方案:别只往一个方向走,来回扫!
// 奇数趟:从左→右,把最大值推到右边
// 偶数趟:从右→左,把最小值拉到左边
// 来回震荡,像调酒师摇雪克杯一样,乌龟和兔子都能照顾到。
//
// 【效果】
// 完美解决"乌龟"数据拖慢的问题,外层总趟数大幅减少。
// 例如上面的例子,第二次从右→左时 1 直接跳到最左边:
// [2,3,4,5,1,6,9] 右→左 → [1,2,3,4,5,6,9] 两趟搞定!
//
// ============================================================
void bubble_sort_v3_cocktail(vector<int>& arr)
{
int n = arr.size();
int left = 0; // 左边界
int right = n - 1; // 右边界
while (left < right)
{
// -------- 第一趟:从左 → 右(把最大值推到右边)--------
int lastSwapRight = left; // 初始化为 left,表示"本轮未发生交换"
for (int j = left; j < right; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
lastSwapRight = j; // 记录最后一次交换的位置
}
}
// 若没有发生交换 → 区间内已全部有序
if (lastSwapRight == left)
{
break;
}
right = lastSwapRight; // 右边界收缩到最后交换位置
// -------- 第二趟:从右 → 左(把最小值拉到左边)--------
int lastSwapLeft = right; // 初始化为 right,表示"本轮未发生交换"
for (int j = right; j > left; j--)
{
if (arr[j - 1] > arr[j])
{
swap(arr[j - 1], arr[j]); // 较小值向左移动
lastSwapLeft = j; // 记录最后一次交换的位置
}
}
// 若没有发生交换 → 区间内已全部有序
if (lastSwapLeft == right)
{
break;
}
left = lastSwapLeft; // 左边界收缩到最后交换位置
}
}
// ============================================================
// 方案四:终极完全体 —— 双向冒泡 + 双向边界收缩
// (Optimized Cocktail Sort with Dual Boundary Shrinking)
// ============================================================
//
// 【优化思路】
// 方案三虽然来回走,但左边界收缩还不够精细——从右往左走时,
// 也应当精确记录"最后一次交换的位置"来更新左边界。
//
// 方案四 = 方案二 + 方案三 的完全融合:
// - 从左→右:记录 lastSwapRight → 更新右边界
// - 从右→左:记录 lastSwapLeft → 更新左边界
// 左右两个边界像钳子一样,从两端向中间"混乱区"迅速收缩。
//
// 【物理直觉】
// 两个边界像两把手术钳,精准地夹住无序区域:
// 左钳子(leftBound): 往右收缩 → 左边已经有序
// 右钳子(rightBound):往左收缩 → 右边已经有序
// 中间是唯一的"需要处理的无序区"
// 每轮两把钳子同时向中间逼近,直到夹紧(left ≥ right)。
//
// 【效果】
// 这是冒泡排序在 O(n²) 家族中能达到的性能巅峰。
// - 同时解决"兔子与乌龟"的不对称问题
// - 两边边界都做精准收缩,避免一切无效比较
// - 对于大部分已有序的数组,复杂度接近 O(n)
//
// 【复杂度】
// 最好(已有序):双向各走一次就收工 → O(n)
// 最坏(逆序):仍然 O(n²),但常数因子比原始冒泡小得多
//
// ============================================================
void bubble_sort_v4_ultimate(vector<int>& arr)
{
int n = arr.size();
int leftBound = 0; // 左边界(已排序区)
int rightBound = n - 1; // 右边界(已排序区)
while (leftBound < rightBound)
{
// 本轮记录的最后交换位置
// 初始化为各自的边界值,表示"本轮未发生任何交换"
int lastSwapRight = leftBound; // 左→右:未交换则保持 leftBound
int lastSwapLeft = rightBound; // 右→左:未交换则保持 rightBound
// -------- 第一趟:从左 → 右(把最大值推到右边)--------
for (int j = leftBound; j < rightBound; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
lastSwapRight = j; // 记录本轮最后的交换位置
}
}
// 如果 lastSwapRight 没变 → 没有发生任何交换 → 区间已有序!
if (lastSwapRight == leftBound)
{
break;
}
// 右边界收缩到本轮最后一次发生交换的位置
rightBound = lastSwapRight;
// -------- 第二趟:从右 → 左(把最小值拉到左边)--------
for (int j = rightBound; j > leftBound; j--)
{
if (arr[j - 1] > arr[j]) // 左 > 右 → 交换
{
swap(arr[j - 1], arr[j]); // 较小值向左移动
lastSwapLeft = j; // 记录本轮最后的交换位置
}
}
// 如果 lastSwapLeft 没变 → 没有发生任何交换 → 区间已有序!
if (lastSwapLeft == rightBound)
{
break;
}
// 左边界收缩到本轮最后一次发生交换的位置
leftBound = lastSwapLeft;
}
}
// ============================================================
//
// 快速排序(Quick Sort)系列
//
// ============================================================
//
// 【核心思想 —— 分治法(Divide and Conquer)】
// 1. 选一个"基准"(Pivot / 老大)
// 2. 分区(Partition):小于基准的放左边,大于基准的放右边
// 3. 对左右两个子数组递归进行同样操作
// 4. 递归到底(只剩 1 个元素)时,整个数组就有序了
//
// 核心公式:QuickSort(arr) = Partition + QuickSort(左) + QuickSort(右)
//
// 【基础版快排的致命弱点】
// 如果总是选最右(或最左)元素当 pivot:
// - 已有序数组 → 每次都分出 0:n-1 → 退化为 O(n²)
// - 完全逆序数组 → 同样退化
// 下面三个优化方案逐一攻破这些弱点。
//
// 【复杂度】
// 基础版:平均 O(n log n),最坏 O(n²)
// 优化后:平均 O(n log n),最坏几乎不会出现
// 空间:递归栈 O(log n)(优化后)
//
// ============================================================
// ----------------------------------------------------------
// 区间插入排序辅助函数:对 arr[left..right] 做插入排序
// 用于方案二的小区间切换
// ----------------------------------------------------------
void insertionSortRange(vector<int>& arr, int left, int right)
{
for (int i = left + 1; i <= right; i++)
{
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// ============================================================
// 基础版快速排序(Basic Quick Sort)
// ============================================================
//
// 最简单的实现:总是选 arr[right] 作为 pivot,然后双向扫描分区。
//
// 【分区流程(Hoare 变体的简化版)】
// pivot = arr[right]
// i 从左边找 ≥ pivot 的元素
// j 从右边找 ≤ pivot 的元素
// 找到后交换,i 和 j 交错时停止
//
// 【痛点演示】arr = [1,2,3,4,5,6,7,8](已有序)
// 选 arr[7]=8 为 pivot:
// 分区结果:左边 [1,2,3,4,5,6,7],右边 [](空!)
// 递归左 7 个,右 0 个 → 极度不平衡 → O(n²)
//
// ============================================================
int basicPartition(vector<int>& arr, int left, int right)
{
int pivot = arr[right]; // 选最右为基准
int i = left; // i:从左扫描,找 ≥ pivot 的
for (int j = left; j < right; j++) // j:从左扫描到 right-1
{
if (arr[j] < pivot) // 比基准小 → 换到左边去
{
swap(arr[i], arr[j]);
i++; // i 左边全是 < pivot 的
}
}
swap(arr[i], arr[right]); // 把 pivot 放到正确位置
return i; // 返回 pivot 的最终位置
}
void quickSort_basic(vector<int>& arr, int left, int right)
{
if (left >= right)
{
return;
}
int pivotIndex = basicPartition(arr, left, right);
quickSort_basic(arr, left, pivotIndex - 1); // 递归左半
quickSort_basic(arr, pivotIndex + 1, right); // 递归右半
}
void quickSort_basic(vector<int>& arr)
{
quickSort_basic(arr, 0, arr.size() - 1);
}
// ============================================================
// 方案一:三数取中法(Median-of-Three Pivot Selection)
// ============================================================
//
// 【优化思路】
// 不再"死板地选最右边当老大",而是取三个位置的元素:
// arr[left](最左)、arr[mid](中间)、arr[right](最右)
// 选它们三个的中位数(不最大也不最小的那个)作为 pivot。
//
// 【为什么选中位数?】
// 中位数保证:左边至少有一部分比它小,右边至少有一部分比它大。
// 这样就几乎不会出现"一边全空"的极端情况。
//
// 【物理直觉】
// 从队伍的最左边、中间、最右边各拉一个人出来比身高,
// 选中间身高的当队长(pivot),然后把它换到最右边去。
// 这样选出的队长大概率是"中等水平",不会太极端。
//
// 【效果】
// 已有序数组:选中间值 → 左右各约一半 → O(n log n) ✓
// 对随机数组也有轻微提升(避免偶尔选到极值)
//
// ============================================================
// 三数取中:比较 left, mid, right 三个位置,把中位数换到 arr[right]
// 这样后续可以直接用标准 Lomuto partition(pivot 始终在 arr[right])
void medianOfThree(vector<int>& arr, int left, int right)
{
int mid = left + (right - left) / 2;
// 三次比较后,arr[mid] 的值就是 left/mid/right 三者的中位数
if (arr[left] > arr[mid]) swap(arr[left], arr[mid]);
if (arr[left] > arr[right]) swap(arr[left], arr[right]);
if (arr[mid] > arr[right]) swap(arr[mid], arr[right]);
// 把中位数换到 arr[right],作为后续 partition 的 pivot
swap(arr[mid], arr[right]);
}
int partition_v1_median(vector<int>& arr, int left, int right)
{
medianOfThree(arr, left, right); // 中位数已放到 arr[right]
int pivot = arr[right];
int i = left;
for (int j = left; j < right; j++)
{
if (arr[j] < pivot)
{
swap(arr[i], arr[j]);
i++;
}
}
swap(arr[i], arr[right]); // pivot 归位
return i;
}
void quickSort_v1_median(vector<int>& arr, int left, int right)
{
if (left >= right)
{
return;
}
int pi = partition_v1_median(arr, left, right);
quickSort_v1_median(arr, left, pi - 1);
quickSort_v1_median(arr, pi + 1, right);
}
void quickSort_v1_median(vector<int>& arr)
{
quickSort_v1_median(arr, 0, arr.size() - 1);
}
// ============================================================
// 方案二:小区间切换插入排序(Insertion Sort Cutoff)
// ============================================================
//
// 【优化思路】
// 快排递归到底层时,子数组非常小(比如 10 个元素以内)。
// 此时函数调用的开销(压栈/弹栈)反而比排序本身还大!
// 而且插入排序对"基本有序"的小数组极快(常数因子小)。
//
// 【物理直觉】
// 大部队用快排(分治效率高),
// 小队(≤10人)直接用插入排序(简单高效),
// 杀鸡不用牛刀!
//
// "递归树底层有大量叶子节点,砍掉它们能省一半开销。"
//
// 【效果】
// 砍掉递归树底层的大量节点,整体性能提升约 10%~15%。
// 经验阈值通常在 5~20 之间,这里选 16。
//
// ============================================================
const int INSERTION_CUTOFF = 16; // 小数组切换阈值
int partition_v2_cutoff(vector<int>& arr, int left, int right)
{
int pivot = arr[right];
int i = left;
for (int j = left; j < right; j++)
{
if (arr[j] < pivot)
{
swap(arr[i], arr[j]);
i++;
}
}
swap(arr[i], arr[right]);
return i;
}
void quickSort_v2_cutoff(vector<int>& arr, int left, int right)
{
// 小区间切换:如果元素数 ≤ 阈值,直接用插入排序
if (right - left + 1 <= INSERTION_CUTOFF)
{
insertionSortRange(arr, left, right);
return;
}
int pi = partition_v2_cutoff(arr, left, right);
quickSort_v2_cutoff(arr, left, pi - 1);
quickSort_v2_cutoff(arr, pi + 1, right);
}
void quickSort_v2_cutoff(vector<int>& arr)
{
quickSort_v2_cutoff(arr, 0, arr.size() - 1);
}
// ============================================================
// 方案三:三路划分 —— 荷兰国旗问题
// (3-Way Partitioning / Dutch National Flag Problem)
// ============================================================
//
// 【优化思路】
// 基础快排只分"小于 pivot"和"大于 pivot"两拨。
// 如果数组中有大量重复元素(如 [4,2,4,4,1,4,4,7]),
// 等于 pivot 的元素会被反复搬来搬去,白白浪费时间。
//
// 三路划分把数组分成三拨:
// 左区:< pivot (lt 标记右边界)
// 中区:== pivot (全部聚在中间,不需要再递归!)
// 右区:> pivot (gt 标记左边界)
//
// 下一次递归时,只需要处理"左区"和"右区",中间庞大的一坨
// 直接跳过,对于重复元素多的场景速度飞升!
//
// 【为什么叫"荷兰国旗问题"?】
// 荷兰国旗由红、白、蓝三色条纹组成。
// 图灵奖得主 Dijkstra 提出这个算法,
// 把数组分成三块(小/等/大)→ 就像荷兰国旗的三色条纹。
// 只用一次遍历 + 三个指针就能完成,不需额外数组。
//
// 【三指针详解】
// lt —— "小于区"的右边界(less than)
// arr[left .. lt-1] 全是 < pivot 的元素
//
// gt —— "大于区"的左边界(greater than)
// arr[gt+1 .. right] 全是 > pivot 的元素
//
// i —— 侦察兵 / 扫描指针
// arr[lt .. i-1] 全是 == pivot 的元素
// arr[i .. gt] 是待扫描区域
//
// 【算法流程(一次遍历)】
// while (i <= gt) // 侦察兵还没撞上大于区边界
// {
// if arr[i] < pivot:
// swap(arr[lt], arr[i]); // 塞到小于区末尾
// lt++; i++; // 小于区扩张,侦察兵前进
//
// else if arr[i] > pivot:
// swap(arr[gt], arr[i]); // 丢到大于区开头
// gt--; // 大于区扩张,i 不动!
// // ↑ i 不动是因为换过来的新元素还没检查过
//
// else: // arr[i] == pivot
// i++; // 等于区自然扩张,侦察兵继续向前
// }
//
// 【为什么 swap(arr[gt], arr[i]) 后 i 不能前进?】
// 换过来的 arr[gt] 是"未扫描区"的元素,它的值是未知的!
// 需要下一轮循环对新的 arr[i] 重新判断。
// 而 swap(arr[lt], arr[i]) 后 i 可以前进,因为换过来的一定是
// 等于区里的元素(已经在之前被 i 扫描并判定为 == pivot 了)。
//
// 【效果】
// - 所有等于 pivot 的元素一次性归位,后续不参与递归
// - 大量重复元素的数组:接近 O(n)
// - 例如 [4,2,4,4,1,4,4,7] 选 pivot=4:
// 三路划分后 → 左区[2,1] 中区[4,4,4,4,4] 右区[7]
// 只需递归 [2,1] 和 [7]!
//
// ============================================================
// 三路划分:返回 pair{lt, gt},区间 [lt, gt] 内都是 == pivot
pair<int, int> threeWayPartition(vector<int>& arr, int left, int right)
{
int pivot = arr[right]; // 基准值
int lt = left; // 小于区的右边界
int gt = right; // 大于区的左边界
int i = left; // 侦察兵
while (i <= gt) // 侦察兵没撞上大于区就继续
{
if (arr[i] < pivot)
{
swap(arr[lt], arr[i]); // 塞到小于区
lt++; // 小于区扩张
i++; // 侦察兵前进
}
else if (arr[i] > pivot)
{
swap(arr[gt], arr[i]); // 丢到大于区
gt--; // 大于区扩张,i 不动!
}
else // arr[i] == pivot
{
i++; // 等于区自动扩张
}
}
return {lt, gt}; // [lt, gt] = 等于区间,不需要再排序
}
void quickSort_v3_threeway(vector<int>& arr, int left, int right)
{
if (left >= right)
{
return;
}
pair<int, int> p = threeWayPartition(arr, left, right);
int lt = p.first;
int gt = p.second;
// 只需要递归小于区和大于区,等于区 [lt, gt] 已经全在正确位置
quickSort_v3_threeway(arr, left, lt - 1);
quickSort_v3_threeway(arr, gt + 1, right);
}
void quickSort_v3_threeway(vector<int>& arr)
{
quickSort_v3_threeway(arr, 0, arr.size() - 1);
}
// ============================================================
// 方案四:终极完全体 —— 三合一优化快排
// (Ultimate Quick Sort: 三数取中 + 小区间切换 + 三路划分)
// ============================================================
//
// 【优化思路】
// 集大成者!把三种优化全部融入:
// 1. 三数取中选 pivot → 永久避免 O(n²) 退化
// 2. 小区间切换插入排序 → 砍掉递归底层海量开销
// 3. 三路划分 → 重复元素直接聚中跳过
//
// 【效果】
// 这是通用排序算法在工程实践中的巅峰形态!
// 事实上,Java 的 Arrays.sort()(基本类型)、C++ 的
// std::sort()(introsort)都综合了这些优化思想。
//
// - 随机数组: O(n log n),常数因子极小
// - 已有序数组: O(n)(三数取中 + 小区间插入排序)
// - 大量重复元素: 接近 O(n)(三路划分跳过重复)
// - 任何数据分布: 稳定高效,没有明显弱点
//
// ============================================================
void quickSort_v4_ultimate(vector<int>& arr, int left, int right)
{
// -------- 优化 1:小区间切换插入排序 --------
if (right - left + 1 <= INSERTION_CUTOFF)
{
insertionSortRange(arr, left, right);
return;
}
// -------- 优化 2:三数取中选 pivot --------
medianOfThree(arr, left, right); // 中位数已放到 arr[right]
int pivot = arr[right];
// -------- 优化 3:三路划分 --------
int lt = left;
int gt = right;
int i = left;
while (i <= gt)
{
if (arr[i] < pivot)
{
swap(arr[lt], arr[i]);
lt++;
i++;
}
else if (arr[i] > pivot)
{
swap(arr[gt], arr[i]);
gt--;
}
else
{
i++;
}
}
// 中间 [lt, gt] 全等于 pivot,跳过!
quickSort_v4_ultimate(arr, left, lt - 1);
quickSort_v4_ultimate(arr, gt + 1, right);
}
void quickSort_v4_ultimate(vector<int>& arr)
{
quickSort_v4_ultimate(arr, 0, arr.size() - 1);
}
// ============================================================
//
// 归并排序(Merge Sort)系列
//
// ============================================================
//
// 【核心思想 —— 分治法(Divide and Conquer)】
// 1. 分(Divide):把数组从中间切成两半
// 2. 治(Conquer):递归地对左右两半分别排序
// 3. 合(Combine):把两个有序子数组合并成一个有序数组
//
// 核心公式:MergeSort(arr) = Merge(MergeSort(左), MergeSort(右))
//
// 与快排的分治对比:
// 快排:分在"治"之前(先分区再递归)→ 自顶向下
// 归并:分为先(先切半递归),合才是核心 → 自底向上回溯
//
// 【归并排序的优势】
// 1. 稳定排序:相等元素保持原始顺序(快排不稳定!)
// 2. 保证 O(n log n):不存在退化到 O(n²) 的情况
// 3. 适合链表排序:链表合并不需要额外空间
// 4. 适合外部排序:处理海量数据(磁盘文件排序)
//
// 【归并排序的劣势】
// 1. 需要 O(n) 额外空间(临时数组)
// 2. 递归版本有栈开销
//
// 【合并过程(Merge)详解 —— 双指针法】
//
// 左右两半都已有序,用两个指针 i、j 分别指向两半的起始位置:
//
// left: [2, 5, 8] right: [1, 3, 9]
// ↑ ↑
// i j
//
// 比较 arr[i] 和 arr[j],较小的放入临时数组,对应指针前进:
//
// 步骤1: 2>1 → 放 1, j++ temp=[1] i→2, j→3
// 步骤2: 2<3 → 放 2, i++ temp=[1,2] i→5, j→3
// 步骤3: 5>3 → 放 3, j++ temp=[1,2,3] i→5, j→9
// 步骤4: 5<9 → 放 5, i++ temp=[1,2,3,5] i→8, j→9
// 步骤5: 8<9 → 放 8, i++ temp=[1,2,3,5,8] i 越界
// 步骤6: i 越界,把右半剩余 [9] 全部追加
// temp=[1,2,3,5,8,9]
//
// 最后把 temp 拷回原数组。O(n) 时间,O(n) 空间。
//
// 【复杂度】
// 时间:T(n) = 2·T(n/2) + O(n) → O(n log n),所有情况一致
// 空间:O(n)(临时数组)+ O(log n)(递归栈)
// 稳定:是(合并时相等元素保持原序)
//
// ============================================================
// ----------------------------------------------------------
// 合并两个有序子数组 arr[left..mid] 和 arr[mid+1..right]
// 使用临时数组 temp,合并后拷回 arr[left..right]
// ----------------------------------------------------------
void merge(vector<int>& arr, int left, int mid, int right)
{
// 临时数组存放合并结果
vector<int> temp(right - left + 1);
int i = left; // 左半指针:arr[left..mid]
int j = mid + 1; // 右半指针:arr[mid+1..right]
int k = 0; // temp 指针
// 双指针归并:每次取较小的放入 temp
while (i <= mid && j <= right)
{
if (arr[i] <= arr[j]) // 用 <= 保证稳定性(左半元素优先)
{
temp[k++] = arr[i++];
}
else
{
temp[k++] = arr[j++];
}
}
// 左半有剩余 → 直接追加(右半已空)
while (i <= mid)
{
temp[k++] = arr[i++];
}
// 右半有剩余 → 直接追加(左半已空)
while (j <= right)
{
temp[k++] = arr[j++];
}
// 把合并好的 temp 拷回原数组
for (size_t t = 0; t < temp.size(); t++)
{
arr[left + static_cast<int>(t)] = temp[t];
}
}
// ============================================================
// 基础版归并排序(自顶向下递归)
// ============================================================
//
// 纯粹的递归分治,代码最简洁,但递归到底层开销大。
//
// ============================================================
void mergeSort_basic(vector<int>& arr, int left, int right)
{
if (left >= right)
{
return; // 只剩 0 或 1 个元素,自然有序
}
int mid = left + (right - left) / 2; // 防止 (left+right) 溢出
mergeSort_basic(arr, left, mid); // 递归排序左半
mergeSort_basic(arr, mid + 1, right); // 递归排序右半
merge(arr, left, mid, right); // 合并两个有序半区
}
void mergeSort_basic(vector<int>& arr)
{
mergeSort_basic(arr, 0, arr.size() - 1);
}
// ============================================================
//
// 桶排序(Bucket Sort)
//
// ============================================================
//
// 【核心思想】
// 把数据分散到若干个"桶"里,每个桶单独排序(通常用插入排序,
// 因为桶内数据量小),最后把所有桶按顺序拼接起来。
//
// 公式:BucketSort(arr) = 分桶 + Σ 桶内排序 + 拼接
//
// 【物理直觉】
// 你是一年级老师,要把全班 50 份试卷按分数排序。
// 你不会直接 50 份一起排,而是:
// 1. 在桌上摆 10 个盒子(桶):0-9分、10-19分、...、90-100分
// 2. 把每份试卷扔进对应盒子(分桶,O(n))
// 3. 每个盒子内部单独排序(桶内排序,每桶数据很少)
// 4. 从低分到高分,把盒子里的试卷依次摞好(拼接,O(n))
//
// 【时间复杂度】
// 设 n 个元素,m 个桶:
// 分桶: O(n) —— 每个元素计算一次桶编号
// 桶内排序: O(m × (n/m)²) —— 每个桶约 n/m 个元素,插入排序
// 拼接: O(n) —— 依次收集
// 总计:O(n + n²/m)
// 当 m ≈ n 时(桶数接近元素数)→ 接近 O(n)!
//
// 最好: O(n) 数据均匀分布,每个桶 1~2 个元素
// 最坏: O(n²) 所有数据挤进同一个桶(退化为插入排序)
// 平均: O(n) 数据较均匀时
//
// 【与计数排序/基数排序的关系】
// 三者都是"非比较排序",理论上可以突破 O(n log n) 下界:
// 计数排序:每个值一个桶(桶数 = 值域范围)
// 基数排序:按数位分桶,多轮处理
// 桶排序: 按值域区间分桶,桶内用比较排序
// 桶排序最灵活——它不是严格比较,也不要求值域连续。
//
// 【适用场景】
// - 数据均匀分布在某个区间(如 [0, 1) 浮点数)
// - 数据量大但值域可控
// - 外部排序(数据分布在多个文件,各自排序后再归并)
//
// 【稳定性】稳定 —— 桶内用稳定排序(如插入排序),桶间按序拼接
//
// 【空间复杂度】O(n + m) —— n 个元素 + m 个桶
//
// ============================================================
void bucketSort(vector<int>& arr)
{
int n = arr.size();
if (n <= 1)
{
return;
}
// ---------- 第一步:找到值域范围 [minVal, maxVal] ----------
int minVal = arr[0];
int maxVal = arr[0];
for (int i = 1; i < n; i++)
{
if (arr[i] < minVal)
{
minVal = arr[i];
}
if (arr[i] > maxVal)
{
maxVal = arr[i];
}
}
// 如果所有元素都相同(minVal == maxVal),已经有序
if (minVal == maxVal)
{
return;
}
// ---------- 第二步:创建桶 ----------
// 桶的数量通常选 n,使得平均每个桶 1 个元素,接近 O(n)
int bucketCount = n;
// vector<vector<int>> 是 C++11 语法(需要空格避免 >> 被解析为右移)
vector<vector<int> > buckets(bucketCount);
// ---------- 第三步:把元素放进桶 ----------
// 关键:如何计算元素属于哪个桶?
//
// 值域跨度 = maxVal - minVal
// 每个桶覆盖的区间大小 = (maxVal - minVal + 1) / bucketCount
//
// 映射公式:
// bucketIndex = (val - minVal) * bucketCount / (maxVal - minVal + 1)
//
// 举例:range = [0, 100], n = 10 个桶
// val=5 → bucketIndex = (5-0) × 10 / 101 = 0 → 桶 0