归并排序和快速排序的预备知识
一个复杂的问题可以拆解成几个简单的问题,再把每个简单的问题解决掉!——通过两个简单的问题来提前准备下归并排序和快速排序。
问题一
如何合并两个有序的数组为一个新的有序数组?
例如数组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]
解决了这两个问题,接下来我们就可以正式开始归并排序和快速排序了。
补充问题:想一下问题二中为何要选定最右侧数据当作基准值,随机选一个是否可以?