什么是快速排序?
在一个无序的数组中选择一个基数,将数组中小于等于基数的元素放到基数左边,将大于等于基数的元素放到基数右边,不断循环此操作,完成排序。
如何实现快速排序呢?
指针法
排序原理
找到一个基数,左右设置两个指针,右指针找到小于等于基数的元素,左指针找到大于等于基数的元素,两个元素交换位置。循环上述流程,完成排序。
下面是大神博客中指针法的核心代码:
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right) {
//控制right指针比较并左移
while(left<right && arr[right] > pivot){
right--;
}
//控制right指针比较并右移
while( left<right && arr[left] <= pivot) {
left++;
}
//交换left和right指向的元素
if(left<right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}
问题
上述代码本身没有问题,但是却能引发一些疑惑:
- 为什么选择第一个元素作为基数之后,要先移动右指针?先移动左指针行不行?能不能把最后一个元素作为基数?
- 为什么右指针移动时候判断条件是 arr[right] > pivot,左指针移动判断条件是 arr[left] <= pivot ?使用 arr[left] <= pivot 行不行?
- 根据大神博客的思路和算法,可以看出左右指针最终会重合,重合之后要将当前位置的元素和基数交换,这里的交换就牵扯到一个问题:指针重合位置的元素和基数相比谁更大?
思考
如果我们在判断指针移动的条件是 arr[right] >= pivot,arr[left] <= pivot,那么基数在交换过程中位置是没有改变的,就是说基数是没有参与交换。
基于上述条件我们找到四种场景:1、左端点作为基数,先移动右指针;2、左端点为基数,先移动左指针;3、右端点为基数,先移动左指针;4、右端点为基数,先移动右指针。
左指针找到的元素,我们先定义为“交换前左元素”,那么:交换前左元素 >=基数;右指针找到的元素,我们先定义为“交换前右元素”,那么:交换前右元素 <= 基数(元素交换前);
左右指针元素交换后,左指针指向的元素定义为“交换后左元素”,那么:交换后左元素 <=基数;右指针指向的元素定义为“交换后右元素”,那么:交换后右元素 >=基数;
很明显在同一轮循环中:交换前左元素 = 交换后右元素,交换前右元素 = 交换后左元素
1.左端点作为基数,先移动右指针
这种情况下,右指针先找到一个“交换前右元素”(<=基数),等待左指针找到一个“交换前左元素”(>=基数)来交换。
- 场景一:右指针找到了“交换前右元素”,左指针找到了“交换前左元素”
左右元素交换,正常。 - 场景二:右指针没找到“交换前右元素”,右指针与左指针重合
此时左指针右两种情况:1、左指针处于初始位置,重合位置元素 = 基数;2、左指针处于交换后的位置,交换后左元素 <= 基数,重合位置元素 <= 基数。
两种情况下重合位置元素都不大于基数,而基数此时位于最左端,数据交换后仍能保证基数左边数据均小于等于基数,正常。 - 场景三:右指针找到了“交换前右元素”,左指针没找到“交换前左元素”,左指针与右指针重合
重合位置为右指针,因为 交换前右元素 <= 基数 ,而基数此时位于最左端,数据交换后仍能保证基数左边数据均小于等于基数,正常。
2.左端点作为基数,先移动左指针
还是相同的场景
- 场景一:左指针找到了“交换前左元素”,右指针找到了“交换前右元素”
左右元素交换,正常。 - 场景二:左指针没找到“交换前左元素”,左指针与右指针重合
重合位置为右指针,右指针有两种情况:1、处于初始位置,因为右指针还没开始比较,所以初始位置元素是可能大于基数(例如[7,6,5,4,3,8])2、处于交换后的位置,交换后右元素 >=基数。
此时基数位于最左端,而重合位置元素是可能大于基数的,交换后,基数左边会存在大于基数的元素!失败! - 场景三:左指针找到了“交换前左元素”,右指针没找到“交换前右元素”,右指针与左指针重合
此时指针重合位置 为左指针,交换前左元素 >=基数,基数当前在最左边,交换后情况和场景二相同,失败!
3.右端点作为基数,先移动左指针
- 场景一:左指针找到了“交换前左元素”,右指针找到了“交换前右元素”
正常交换,进入下一个循环。 - 场景二:左指针没找到“交换前左元素”,左指针与右指针重合
重合位置为右指针,右指针有两种情况:1、处于初始位置,初始位置 = 基数,没有问题 2、处于交换后的位置,交换后右元素 >=基数。
因为基数在最右边,将重合位置元素和基数交换后,基数右边仍然大于等于基数,正常! - 场景三:左指针找到了“交换前左元素”,右指针没找到“交换前右元素”,右指针与左指针重合
此时指针重合位置 为左指针,交换前左元素 >=基数,与基数交换后,正常。
4.右端点作为基数,先移动右指针
- 场景一:右指针找到了“交换前右元素”,左指针找到了“交换前左元素”
正常交换,继续循环。 - 场景二:右指针没找到“交换前右元素”,右指针与左指针重合
指针重合位置为左指针,此时左指针右两种情况:1、左指针处于初始位置,因为右指针还没开始比较,所以初始位置元素是可能小于基数(例如[2,9,8,7,6,5])2、左指针处于交换后的位置,交换后左元素 <= 基数。
此时基数位于最右端,如果和指针重合位置交换元素,那么基数右边存在小于基数的元素,失败! - 场景三:右指针找到了“交换前右元素”,左指针没找到“交换前左元素”,左指针与右指针重合
重合位置为右指针,因为 交换前右元素 <= 基数 ,而基数此时位于最右端,数据交换后和上述场景相同,失败!
总结
如果我们在判断指针移动的条件是 arr[right] >= pivot,arr[left] <= pivot,那么会造成基数在交换过程中位置是没有改变,这种情况下左右指针移动顺序是要注意的:当选择左端点为基数,就先移动右指针;当选右端点为基数,就先移动左指针。
如果随机选择一个位置作为基数,那不管先移动哪边,应该都有可能出问题。
解决办法
判断指针移动的条件写为 arr[right] > pivot,arr[left] < pivot。
小小的改动,在循环时候就可以使基数本身参与交换,这种写法最终是不需要将指针重合位置和基数交换的,反而更加简洁。
实现代码
//快速排序
public static void mySort(int[] a, int start, int end) {
if (start >= end) {
return;
}
//选择第一个元素作为基数
int base = a[start];
int left = start;
int right = end;
//left != right时交换左右元素
while (left != right) {
if(a[left] == a[right]){
left++;
}
//左边向右移动
while (left < right && a[left] < base) {
left++;
}
//右边向左移动
while (left < right && a[right] > base) {
right--;
}
//左右元素互换
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
//对基数左右部分进行递归调用
mySort(a, start, left - 1);
mySort(a, left + 1, end);
}
public static void main(String[] args) {
int[] a = {4,3,2,1,5,6,7};
mySort(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
填坑法
排序原理
填坑法是先找到一个数作为基数,基数位置作为一个坑,通过指针移动找到合适的值放到坑里,这个指针位置成为新的坑。
例如,将左端点作为基数,此时左端点就是第一个坑,移动右指针,右指针找到一个小于等于基数的元素,将元素复制到基数的位置(最左端),而右指针指向的位置变为第二个坑,等待新元素填充。这时左指针移动,寻找大于等于基数的元素,将元素赋值到坑里(右指针当前位置),左指针作为新的坑。不断循环,知道左右指针重合,将基数填到重合的位置。
问题
- 什么位置是坑位?
- 什么情况下会改变坑位?
思考
- 不移动的指针指向的是坑位
- 发生“填坑”操作会导致坑位改变
那又涉及到基数选择和指针移动顺序的问题了!
如果选择左端点作为基数,那左指针现在就是坑位了,那它是坑位了,就应该先移动右指针去找元素来“填坑”!
如果选择右端点作为基数,那就只能先移动左指针了。
但是如果左端点作为基数,先移动左指针了,或者右端点作为基数,先移动右指针了,会发生什么呢?
选一边端点作为基数,同时先移动同一边指针
1.判断指针移动的条件为 arr[right] > pivot,arr[left] < pivot
这种情况下左指针会先自己填自己一次,其实是没有必要的:
代码:
public static void mySort(int[] a, int start, int end) {
int left = start;
int base = a[start];
int right = end;
int index = start;
while (left < right) {
if (a[left] == a[right]) {
right--;
}
//左边移动
while (right > left) {
if (a[left] < base) {
left++;
} else {
//填坑
a[index] = a[left];
index = left;
System.out.println(Arrays.toString(a));
break;
}
}
//右边移动
while (right > left) {
if (a[right] > base) {
right--;
} else {
//填坑
a[index] = a[right];
index = right;
System.out.println(Arrays.toString(a));
break;
}
}
}
//填入基数
a[index] = base;
System.out.println(Arrays.toString(a));
//进入递归
if (index - 1 > start) {
mySort(a, start, index - 1);
}
if (end > index + 1) {
mySort(a, index + 1, end);
}
}
public static void main(String[] args) {
// int[] a = {7, 8, 5, 4, 4, 2, 3, 6, 9, 0};
int []a = {7,6,5,4,4,8};
mySort(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
运行结果:自己先填充自己一次,填充后和排序前没有任何改变
2.判断指针移动的条件为 arr[right] >= pivot,arr[left] <= pivot
会造成指针重合位置不是坑位,例如[7,6,5,4,3,8],左指针会一路跑到8的位置,很明显8的位置根本就不是坑位。
选择一边为基点,另一边指针先移动
代码:
public static void mySort(int[] a, int start, int end) {
int left = start;
int base = a[start];
int right = end;
int index = start;
while (left < right) {
if (a[left] == a[right]) {
left++;
}
//右边移动
while (right > left) {
if (a[right] >= base) {
right--;
} else {
//填坑
a[index] = a[right];
index = right;
System.out.println(Arrays.toString(a));
break;
}
}
//左边移动
while (right > left) {
if (a[left] <= base) {
left++;
} else {
//填坑
a[index] = a[left];
index = left;
System.out.println(Arrays.toString(a));
break;
}
}
}
//填入基数
a[index] = base;
System.out.println(Arrays.toString(a));
//进入递归
if (index - 1 > start) {
mySort(a, start, index - 1);
}
if (end > index + 1) {
mySort(a, index + 1, end);
}
}
目前没发现问题。
总结
填坑法也建议:选择左端点作为基点,先移动右指针;选择右端点为基点,就先移动左指针。
全文总结
不论是指针法还是填坑法,都建议选择一个端点作为基点,然后先移动另一边的指针。而判断指针移动的条件为 arr[right] > pivot,arr[left] < pivot。
如果推理过程有错,请留言,我及时改正。