应用——>排序,什么意思?-------------> 排序是一种应用,算法的应用,本身不是算法
排序的手段——>交换,什么意思?-------> 交换是排序的本质操作
算法思想——>怎么交换,什么意思?-----> 好的算法实现少量的元素交换,不高明的算法实现大量的元素交换
怎么排序 取决于 怎么交换,什么意思?--> 比较一下 冒泡 和 选择排序,你就会体悟了~~~
自己比较么?当然了~我这篇只讲高级一点的快排,而且,美需要用心自己发现。
言归正传......
一组数字 从大到小 进行快速排序 的思想:
给定一个乱序数列,
从中任意选出一个元素【选择的策略:可以随机选,也可以每次选数组的首元素,我们用单词key或者pivot表示】,
通过 算法 实现元素交换【交换思想:比key/pivot大的元素向整个数组的右侧靠拢,比key/pivot小的元素向整个数组的左侧靠拢,所以这个任意选出的元素key/pivot被称之为是枢纽,因为它是小元素与大元素的分界线】
使得最后
Pivot的左边都是比它小的数据,
Pivot的右边都是比它大的数据。
至此,对pivot的左、右数列分别重复上述过程【策略:尾递归】,
最后整个数列有序。
不管快速排序的代码最终怎么实现,快排的思想始终不变,都是选定一个枢纽后,通过交换,让大的在其右,小的在其左,之后对枢纽的左右序列重复上述过程,只不过实现的策略因人而已。
下面 听听 我给大家讲讲故事,通过怎么个找枢纽(这是小问题),选定了枢纽后,怎么个交换大小元素(这是大问题),最后让一个无序的数组齐刷刷地摆在你的面前。
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 8 | 9 | 10 | 1 | 3 | 5 | 4 | 7 |
实现快速排序的策略1:
从Arr[9]开始向左找到第1个比Key = 6小的数是Arr[8] = 4,把4覆盖到Arr[0]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 8 | 9 | 10 | 1 | 3 | 5 | 4 | 7 |
从Arr[0]开始向右找到第1个比Key = 6大的数是Arr[2] = 8,把8覆盖到Arr[8]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 8 | 9 | 10 | 1 | 3 | 5 | 8 | 7 |
从Arr[8]开始向左找到第1个比Key = 6小的数是Arr[7] = 5,把5覆盖到Arr[2]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 9 | 10 | 1 | 3 | 5 | 8 | 7 |
从Arr[2]开始向右找到第1个比Key = 6大的数是Arr[3] = 9,把9覆盖到Arr[7]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 9 | 10 | 1 | 3 | 9 | 8 | 7 |
从Arr[7]开始向左找到第1个比Key = 6小的数是Arr[6] = 3,把3覆盖到Arr[3]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 3 | 10 | 1 | 3 | 9 | 8 | 7 |
从Arr[3]开始向右找到第1个比Key = 6大的数是Arr[4] = 10,把10覆盖到Arr[6]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 3 | 10 | 1 | 10 | 9 | 8 | 7 |
从Arr[6]开始向左找到第1个比Key = 6小的数是Arr[5] = 1,把1覆盖到Arr[4]上,
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 3 | 1 | 1 | 10 | 9 | 8 | 7 |
从Arr[4]开始向右找到第1个比Key = 6大的数:
Arr[4] = 1 < 6
Arr[5],走到这里,整个数组完整遍历一遍,把Key = 6赋值给Arr[5],
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 4 | 2 | 5 | 3 | 1 | 6 | 10 | 9 | 8 | 7 |
上面的过程被称之partition,也即一趟快排,
因为上面的过程使得小于6的元素都跑到了6的左边,大于6的元素都跑到了6的右边,
也因此使得元素6放在了最终确定的位置上。
Partition过程的时间复杂度是O(n)。
而整个 快排 的时间复杂度是0(n*logn),所以快排共需要logn数量级趟partition。
之后分别对Arr[0]-Arr[4]即{4,2,5,3,1} 和 Arr[6]-Arr[9]即{10,9,8,7}进行同思路的找枢纽 和 partition过程。
所以,整个 快排 是通过 尾递归 实现的。
如下是C代码实现:
#include <stdio.h>
void print_arr(int *arr, int length)
{
for(int i=0;i<length;i++){
printf("%d ",arr[i]);
}
printf("\n");
}
void sort(int *a, int left, int right)
{
if(left >= right){
return;
}
int i = left;
int j = right;
int key = a[left];
while(i<j){// 一次此while内部执行,就是一趟partition
while(i<j && key<=a[j])
j--;
a[i] = a[j];
while(i<j && key>=a[i])
i++;
a[j] = a[i];
}
a[i] = key;
print_arr(a,10);
sort(a,left,i-1);
sort(a,i+1,right);
}
int main()
{
int arr[10] = {6,2,8,9,10,1,3,5,4,7};
print_arr(arr,10);
printf("*****************************\n");
sort(arr,0,9);
printf("*****************************\n");
print_arr(arr,10);
printf("*****************************\n");
}
程序的执行结果:
初始数组:6 2 8 9 10 1 3 5 4 7
partition 1st 结果:4 2 5 3 1 6 10 9 8
partition 2nd 结果:1 2 3 4 5 6 10 9 8 7
partition 3rd 结果:同2nd结果
partition 4th 结果:同2nd结果
partition 5th 结果:1 2 3 4 5 6 7 9 8 10
partition 6th 结果:同5th结果
partition 7th 结果:1 2 3 4 5 6 7 8 9 10
还是这个初始数组:
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 8 | 9 | 10 | 1 | 3 | 5 | 4 | 7 |
实现快速排序的策略2:
随机选中数组中的某个元素作为partition过程的枢纽pivot,比如Arr[2] = 8,
选中后,把这个pivot = Arr[2] = 8 与数组的最后一个元素Arr[9] = 7交换,
即我们自己规定,在每次partition之前把选中的key/partition放在数组的最后位置
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 9 | 10 | 1 | 3 | 5 | 4 | 8 |
接着,我们自己定义一个 小于等于区间,里面要存放小于等于pivot的数组元素,
它的首地址就是Arr,其实它是Arr的开头部分,我们用Arr本身来维护这个小于等于区间,
现在它的初始长度为0,里面什么也没有,
我们从Arr[0]开始向后遍历数组:
Arr[0] = 6 < (pivot = 8),需要加入到小于等于区间中,而 小于等于区间 现在长度为0,后面的元素就是Arr[0],
所以把Arr[0]本身和本身进行交换,
同时小于等于区间长度增1,即为1,小于等于区间现在就是Arr的第一个元素;
Arr[1] = 2 < (pivot = 2),小于等于区间 后面的元素就是Arr[1],
所以把Arr[1]本身和本身进行交换,
同时小于等于区间长度增1,为2,小于等于区间现在就是Arr的前两个元素;
Arr[2] = 7 < (pivot = 2),小于等于区间 后面的元素就是Arr[2],
所以把Arr[2]本身和本身进行交换,
同时小于等于区间长度增1,为3,小于等于区间现在就是Arr的前三个元素;
Arr[3] = 9 > (pivot = 2),继续向后遍历;
Arr[4] = 10 > (pivot = 2),继续向后遍历;
Arr[5] = 1 < (pivot = 2),小于等于区间 后面的元素是Arr[3],
把Arr[5]与Arr[3]进行交换,
同时小于等于区间长度增1,为4,
现在Arr为
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 1 | 10 | 9 | 3 | 5 | 4 | 8 |
而 小于等于区间 为其 前4个元素;
Arr[6] = 3 < (pivot = 2),小于等于区间 后面的元素是Arr[4],
把Arr[6]与Arr[4]进行交换,
同时小于等于区间长度增1,为5,
现在Arr为
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 1 | 3 | 9 | 10 | 5 | 4 | 8 |
而小于等于区间 为其 前5个元素;
之后,我简言之,
Arr[7] = 5 < pivot,交换Arr[7]与Arr[5];
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 1 | 3 | 5 | 10 | 9 | 4 | 8 |
Arr[8] = 4 < pivot,交换Arr[8]与Arr[6]
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 1 | 3 | 5 | 4 | 9 | 10 | 8 |
Arr[9] = 8 = pivot,也交换Arr[7]与Arr[9]
数组Arr下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对应元素 | 6 | 2 | 7 | 1 | 3 | 5 | 4 | 8 | 10 | 9 |
至此数组一趟遍历完毕,也完成了一次Partition,
可以发现小于等于区间的最后一个元素一定是pivot,
枢纽8位于其应在在位置Arr[7],
之后对Arr[0]-Arr[6] 和 Arr[8]-Arr[9]进行相同的操作,通过尾递归实现。
如下是java代码实现:
说明:Math.random()返回带正号的double值,范围在[0.0, 1.0)
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int left, int right) {
if (left < right) {
int random = left + (int) (Math.random() * (right - left + 1));
swap(arr, random, right);
int mid = partition(arr, left, right);
process(arr, left, mid - 1);
process(arr, mid + 1, right);
}
}
public static int partition(int[] arr, int left, int right) {
int pivot = left - 1;
int index = left;
while (index <= right) {
if (arr[index] <= arr[right]) {
swap(arr, ++pivot, index);
}
index++;
}
return pivot;
}
public static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}