归并排序和快速排序的预备知识

一个复杂的问题可以拆解成几个简单的问题,再把每个简单的问题解决掉!——通过两个简单的问题来提前准备下归并排序和快速排序。

问题一

如何合并两个有序的数组为一个新的有序数组?

例如数组a为[2 3 6] 数组b为 [1 4 5],两个数组都是从小到大排列的,经过处理后结果为[1 2 3 4 5 6]

最直接方法是把a b数组合并后再排序,这种当然可以,但这样就忽略了每个数组有序性的特性。我们翻译下这个有序性:

特性1 有序数组各个值是递增的,如果一个数小于数组的某一个值,那么肯定也小于数组后续的值。例如a数组中的元素2是小于b数组4的,必然也小于b数组后续的5,相当于减少了后续的判断次数。

特性2 每个数组的第一个值是各自值中最小的。

根据这两个特性我们编写如下代码:

 1 import java.util.Arrays;
 2 
 3 public class MergeTest {
 4     public static void main(String[] args) {
 5         //合并两个有序(都为从小到大)的数组
 6         int[] a = {2, 5, 6};
 7         int[] b = {1, 3, 4};
 8         System.out.println("a" + Arrays.toString(a));
 9         System.out.println("b" + Arrays.toString(b));
10         int[] merge = mergeSortedArray(a, b);
11         System.out.println("*merged" + Arrays.toString(merge));
12     }
13 
14     private static int[] mergeSortedArray(int[] a, int[] b) {
15         //初始化一个新的数组,长度为ab数组之和
16         int[] merge = new int[a.length + b.length];
17         //merge的写入位置 用来记录写入到哪了
18         int mergeIndex = 0;
19         //a数组读取的位置 用来记录读取到哪了
20         int aIndex = 0;
21         //b数组读取的位置
22         int bIndex = 0;
23         //从a数组开始处理(当然从b数组开始也可以)和b数组的值依次比较,谁小就写入merge中,保证了写入的值就是从小到大的
24         for (int i = aIndex; i < a.length; i++) {
25             //a数组元素依次和b数组元素比较
26             for (int j = bIndex; j < b.length; j++) {
27                 System.out.println("a " + a[i] + " b " + b[j]);
28                 //a的元素比b中的小 merge里写a 否则写b
29                 if (a[i] < b[j]) {
30                     System.out.print("\t" + a[i] + "<" + b[j]);
31                     System.out.print(" a mergeIndex " + mergeIndex + " value " + a[i] + " aIndex " + aIndex + " bIndex " + bIndex);
32                     merge[mergeIndex] = a[i];
33                     //写完后,mergeIndex和aIndex都要移动到下一个
34                     mergeIndex++;
35                     aIndex++;
36                     System.out.println(Arrays.toString(merge) + " break");
37                     //由于b中元素是从小到大排列的 只要发现比b中元素小 后续的b元素就无需再比较了,节省了后续的比较开销(特性1)
38                     break;
39                 } else {
40                     System.out.print("\t" + a[i] + ">=" + b[j]);
41                     System.out.print(" b mergeIndex " + mergeIndex + " value " + b[j] + " aIndex " + aIndex + " bIndex " + bIndex);
42                     //小的落在了b数组,那就写它
43                     merge[mergeIndex] = b[j];
44                     //同样写完对应的mergeIndex和bIndex都要移动到下一个
45                     mergeIndex++;
46                     bIndex++;
47                     System.out.println(Arrays.toString(merge));
48                 }
49             }
50         }
51         System.out.println("aIndex " + aIndex + " bIndex " + bIndex + Arrays.toString(merge));
52         //由于a数组中可能存在元素都大于b数组的值,导致a数组后续值未处理(b数组同理),因此检查a数组或b数组是否都处理完,有剩余直接写入到merge中即可。
53         for (; aIndex < a.length; aIndex++) {
54             System.out.println("a mergeIndex " + mergeIndex + " value " + a[aIndex]);
55             merge[mergeIndex] = a[aIndex];
56             mergeIndex++;
57         }
58         for (; bIndex < b.length; bIndex++) {
59             System.out.println("b mergeIndex " + mergeIndex + " value " + b[bIndex]);
60             merge[mergeIndex] = b[bIndex];
61             mergeIndex++;
62         }
63         return merge;
64     }
65 }

输出说明:

a[2, 5, 6]
b[1, 3, 4]
a 2 b 1 #从a第一个元素开始,和b的第一个元素比较
	2>=1 b mergeIndex 0 value 1 aIndex 0 bIndex 0[1, 0, 0, 0, 0, 0] #由于2>1,则把1保存到合并数组里,bIndex向右移动一位
a 2 b 3 #和b的第二个元素比较
	2<3 a mergeIndex 1 value 2 aIndex 0 bIndex 1[1, 2, 0, 0, 0, 0] break #保存2,aIndex右移动一位。由于b为升序排序,后续的元素无需再判断,直接break
a 5 b 3 #开始从a的第二个元素开始比较,b数组也从第二个元素开始
	5>=3 b mergeIndex 2 value 3 aIndex 1 bIndex 1[1, 2, 3, 0, 0, 0] #保存3,bIndex右移动一位
a 5 b 4 #a的第二个元素对比b数组最后一个4
	5>=4 b mergeIndex 3 value 4 aIndex 1 bIndex 2[1, 2, 3, 4, 0, 0] #保存4,bIndex右移动一位
aIndex 1 bIndex 3[1, 2, 3, 4, 0, 0] #循环结束aIndex为1 bIndex为3
a mergeIndex 4 value 5 #aIndex未达到最右侧位置,说明未处理完毕,直接按顺序合并a数组剩余元素至数组即可
a mergeIndex 5 value 6 #继续合并剩余元素
*merged[1, 2, 3, 4, 5, 6] #最终合并完毕

 

问题二

如何处理一个数组,按数组中某个元素分割,使一侧数据都小于这个元素,另外一侧都大于这个元素?

例如数组[2 5 4 6 1 3],假定选取[3],经过处理后为[2 1 3 5 4 6],这时左侧的数据都小于3,右侧数都大于3。

首先想到对整个数组排序,肯定是满足需求的。但其实这个选定数据的两侧数据可以是无序的,只要满足小于或大于选定的数即可,这无疑放宽了排序条件,降低了排序开销。

其实核心问题有两个:

1 如何确定选定值;

2 如何保证选定值两侧数据的大小。

我们先不考虑其他因素,随便选一个数,比如4,接下来移动各个元素,比4大就放到右侧,小的放在左侧,最后根据记录的两侧追加位置再截取数组。

类似这样:

 

但这种带来了额外的内存开销,我们可以通过移动元素位置来避免申请额外内存。首先选定最右侧的4为基准值,从左侧第一个元素开始对比,直到发现大于基准值的数5,暂停移动。这时再从右侧开始往左侧移动,发现小于基准值的数1就暂停移动。这时需要交换5和1,继续移动两边位置和交换位置,直到左右位置重合。最后再把左侧所对应的位置和基准值交换。如图所示:

代码:

 1 import java.util.Arrays;
 2 
 3 public class PartitionTest {
 4     public static void main(String[] args) {
 5         int[] numbers = {2, 5, 6, 3, 1, 4};
 6         System.out.println(Arrays.toString(numbers));
 7         int pivotPos = partition(numbers, 0, numbers.length - 1);
 8         System.out.println("pivotPos " + pivotPos + Arrays.toString(numbers));
 9     }
10 
11     private static int partition(int[] numbers, int leftIndex, int rightIndex) {
12         //选取最右侧的元素为基准值(或者叫做轴、中心值)
13         //基准值所处位置
14         int pivotPos = rightIndex;
15         //基准值
16         int pivot = numbers[pivotPos];
17         //右侧开始位置要去掉基准值的位置
18         rightIndex -= 1;
19         System.out.println("pivot " + pivot + " pivotPos " + pivotPos + " leftIndex " + leftIndex + " rightIndex " + rightIndex);
20         int loopNum = 0;
21         while (true) {
22             System.out.print("loop:" + loopNum + " ");
23             loopNum++;
24             System.out.println("leftIndex " + leftIndex + " rightIndex " + rightIndex);
25             //从左侧开始向右移动依次判断值 只要值小于基准值说明它就是在左侧,继续判断下一个值
26             while (numbers[leftIndex] < pivot) {
27                 System.out.print("\tLeft " + numbers[leftIndex] + "<" + pivot);
28                 leftIndex++;
29                 System.out.println(" leftIndex " + leftIndex);
30             }
31             //同理,右侧的要向左移动依次判断值 只要值大于基准值,就继续向左移动
32             while (numbers[rightIndex] > pivot) {
33                 System.out.print("\tRight" + numbers[rightIndex] + ">" + pivot);
34                 //因为是向左移动,对应的值是不断变小的,所以要减1
35                 rightIndex--;
36                 System.out.println(" rightIndex " + rightIndex);
37                 //右侧位置移动到最左侧后退出此循环
38                 if (rightIndex < 0) {
39                     break;
40                 }
41             }
42             //对比下左右值 如果左侧位置大于等于右侧说明两侧已经相遇,退出循环,否则就交换两者位置,继续移动
43             if (leftIndex >= rightIndex) {
44                 System.out.println(leftIndex + ">=" + rightIndex + " break");
45                 break;
46             } else {
47                 System.out.print("swap leftIndex " + leftIndex + " rightIndex " + rightIndex + Arrays.toString(numbers) + "=>");
48                 int tmp = numbers[rightIndex];
49                 numbers[rightIndex] = numbers[leftIndex];
50                 numbers[leftIndex] = tmp;
51                 System.out.println(Arrays.toString(numbers));
52             }
53         }
54         //最后交换leftIndex的值和基准值,最终把数组分割成两部分,左侧小于基准值,右侧大于基准值
55         System.out.print("swap pivot leftIndex " + leftIndex + " pivotPos " + pivotPos + Arrays.toString(numbers) + "=>");
56         numbers[pivotPos] = numbers[leftIndex];
57         numbers[leftIndex] = pivot;
58         System.out.println(Arrays.toString(numbers));
59         return leftIndex;
60     }
61 }

 输出说明:

[2, 5, 6, 3, 1, 4] #原始数据
pivot 4 pivotPos 5 leftIndex 0 rightIndex 4 #选定最右侧的4为基准值(pivot),从数组左侧开始向右移动,右侧开始位置为基准值的上一个位置(pivotPos-1)
loop:0 leftIndex 0 rightIndex 4 #第一轮左侧位置0 右侧位置4
	Left 2<4 leftIndex 1 #2小于4 左侧当前位置为1,暂停移动
swap leftIndex 1 rightIndex 4[2, 5, 6, 3, 1, 4]=>[2, 1, 6, 3, 5, 4] #这里我们发现右侧位置仍然为4,说明当前rightIndex所对应的值是小于基准值的(可以想想为啥?),因此我们直接交换左右位置对应的值,也就是把数值5和1交换位置
loop:1 leftIndex 1 rightIndex 4 #第二轮左侧位置由于上一轮发现了小于基准值的数,位置要+1
	Left 1<4 leftIndex 2  #1小于4 记录左侧当前位置为2,暂停移动
	Right5>4 rightIndex 3 #5大于4 记录右侧位置3,由于右侧位置是往左侧移动,所以位置要-1,暂停移动
swap leftIndex 2 rightIndex 3[2, 1, 6, 3, 5, 4]=>[2, 1, 3, 6, 5, 4] #再次交换位置
loop:2 leftIndex 2 rightIndex 3 #第三轮
	Left 3<4 leftIndex 3 #记录位置,暂停移动
	Right6>4 rightIndex 2 #记录位置,暂停移动
3>=2 break #左侧位置已经大于或等于右侧位置,说明两侧已经重合,停止循环
swap pivot leftIndex 3 pivotPos 5[2, 1, 3, 6, 5, 4]=>[2, 1, 3, 4, 5, 6] #最后交换左侧到达的最终位置和基准值交换,从而使数组左侧都小于4,右侧都大于4
pivotPos 3[2, 1, 3, 4, 5, 6]

 解决了这两个问题,接下来我们就可以正式开始归并排序和快速排序了。

 

补充问题:想一下问题二中为何要选定最右侧数据当作基准值,随机选一个是否可以?

posted @ 2022-06-17 21:57  binary220615  阅读(32)  评论(0编辑  收藏  举报