1.定义
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.定义理解
由定义可知,快速排序基本思路是先选一个基准值(key),然后以基准值为标准,将待排序序列分为左右两部分,左右两个序列,不要求有序,我们只需满足左边的数(左序列)小于基准值,右边的数(右序列)大于基准值,此时基准值在待排序序列的位置就是排好序的位置,也就是说我们确定了一个元素的位置。然后我们再对左边和右边的数进行同样的操作,左边的数再选一个基准值,然后将大于该基准值的数挪到基准值的右边,小于该基准值的数挪到基准值的左边,此时我们右确定一个基准值的位置,以此类推,直至待排序序列中所有元素位置被却确定。
下面我们把每个基准值都选为这个序列的最左边的值进行说明
当我们选好key了,那我们要用什么方法挪动数据,才能把这个序列分为:小于key的部分,key,大于key的部分。
Hoare给出的思路是,我们创建两个变量,left和right,left这个变量指向待排序数组的最左边的元素,right指向待排序数组最右边的元素
然后右边的right往左遍历,找比key小的元素,找到后停下来,在右边找到比key小的元素之后,左边的left开始往右遍历,left找比key大的元素,找到之后停下来,然后交换left和right在数组中对应的数据,交换之后,right先走,继续找比key小的数据,找到后停下来,然后left找比key大的数据,找到后停下来,然后交换left和right所指向的数据,以此类推,直到right与left相遇,此时说明相遇位置左边都是小于key的,相遇位置右边都是大于key的,然后我们将相遇位置的与keyi对应的key交换,然后一次快排就完成了,此时待排序列被分为了:小于key的部分,key,大于key的部分。
此时一趟快排结束,
结束后,我们对keyi左边的序列按照快排的思路,进行同样的操作,右边的序列也进行同样的操作,直到序列中所有元素位置确定下来。
逻辑缕清后我们来写代码。
3.快排-递归代码
由上面的逻辑我们可以用类似二叉树的前序遍历来实现快排
1.Hoare
#include <stdio.h>
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
int PartSort(int arr[], int left, int right)
{
//三数取中找key
int midi = GetMidNumi(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
//挪数据
while (right > left)
{
while (right > left && arr[right] >= arr[keyi])
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[keyi]);
keyi = left;
return keyi;
}
void QuickSort(int arr[], int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort(arr, left, right);
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
int main()
{
int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8 };
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
QuickSort(arr, 0, n-1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
2.前后指针
#include <stdio.h>
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
int PartSort(int arr[], int left, int right)
{
//三数取中找key
int midi = GetMidNumi(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi])
{
prev++;
Swap(&arr[prev], &arr[cur]);
cur++;
}
else
{
cur++;
}
}
Swap(&arr[keyi], &arr[prev]);
keyi = prev;
return keyi;
}
void QuickSort(int arr[], int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort(arr, left, right);
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
int main()
{
int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8};
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
QuickSort(arr, 0, n-1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
4.快排代码讲解
1.三数取中
我们发现上面的代码中,出现三数取中
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
三数取中是我们为了选择合适的key而想出的一种方法。
假设有个待排序序列为:“9,10,2,7,6,3,4,5,1,8”,如果我们直接将最左边的‘9’作为key进行快速排序
我们看这张图,如果我们选’9‘为key我们发现’10‘和’8‘交换,但是我们思考一下,如果我们想把这个序列排为有序,那么’8‘在这个序列属于靠后位置,但是现在’8‘被交换到了第二个位置,所以’10‘和’8‘的交换并不完美,并且后面的操作还会把’8‘挪回去,这样的挪数据会消耗不必要的时间和空间。
那么如果我们利用三数取中,即’9‘,’6‘,’10‘,我们选取’6‘作为key,并把’6‘与’9‘交换,让’6‘放在最左边作为key
我们可以看出,当’6‘作为key时,在进行一次快速排序后,每对元素交换都比较合理,他们交换后的位置都距离有序时的位置更近了,所以三数取中可以使我们快排的效率提升。
2.为什么right先走
我们发现我们每次都把序列的最左边元素作为key,然后left指向序列的最左边,right指向元素的最右边,我们每次都是让right先去找小,然后left再去找大,其实right先走是为了保证最后和left相遇时,相遇的数比key小,然后我们把相遇的元素与key交换。
这里面一共有三种情况:
- right找小,但是一直没有找到,直接与left相遇,也就是说left没有动,left,right,key都在同一位置,这时我们把相遇位置和key交换,虽然是自己自己交换,但是它还是保证了key的左边比key小,key的右边比key大。
- 当right找小找到了,然后我们的left找大,也找到了,这是我们交换,left和right所指向的元素,交换后right指向的元素大于key,left指向的元素小于key,然后right继续找小,如果找到了继续交换,同样交换后right指向的元素大于key,left指向的元素小于key,然后right继续找小,当right找不到比key小的元素时,right和left相遇,并且相遇位置的元素小于key,然后我们就直接交换相遇位置和key,这样也也是满足快排的要求的。
- 当right找小找到了,但是left找大没有找到,然后left和right相遇,此时我们也保证了相遇位置的元素小于key,然后我们直接将相遇位置的元素与key交换。
以上三种情况,我们都保证了当我们以序列最左边元素为key,right先走,left后走,left和right相遇位置的元素都是小于key的,然后我们可以直接将相遇位置与key交换。
同理,如果我们每次都以序列的最右端的元素为key的话,那我们要让left找大,先走,right找小,后走。
5.快排非递归理解
当我们对序列进行一次快排后我们分割出了:小于key的区间,key,大于key的区间,然后我们还要对小于key的区间,和大于key的区间继续分割,我们每分割一次都能将一个元素的最终位置确定。
如果我们想用非递归实现快排,我们可以先对待排序序列,进行一次快排,排好后待排序序列被分割为小于key的区间,key,大于key的区间,然后我们在对小于key的区间进行快排,同样分割出小于key的区间,key,大于key的区间,然后我们继续对小于key的区间快排,以此类推,直到小于key的区间只有一个元素,但是我们发现,我们一直都在对每次分割后小于key的区间进行排序,大于key的区间没有被排序,当我们把小于key的区间都排好时,我们就找不到每次分割对应的大于key的区间,那我们就需要想办法将大于key的区间存起来。
快排的非递归和递归的基本逻辑是相同的,但是我们用递归的方法时,虽然我们也在不停的对小于key的区间进行快排,但是当小于key的区间排好后,递推结束,回归时,系统帮我们把大于key的区间记录了下来,所以非递归我们要手动把每次分割的大于key的区间记录下来,并且我们要用栈,利用它的后入先出性质。
当快排后分割的区间只有一个元素时就不用入栈了,直接出栈顶原先入的区间,然后继续循环上面操作直到栈空了。
6.快排-非递归代码
#include "stack.h"
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
void QuickSortNonR(int arr[], int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
left = STTop(&st);
STPop(&st);
right = STTop(&st);
STPop(&st);
int begin = left, end = right;
//三数取中找key
int midi = GetMidNumi(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
//挪数据
while (right > left)
{
while (right > left && arr[right] >= arr[keyi])
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[keyi]);
keyi = left;
//区间入栈
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
}
int main()
{
int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8 };
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
QuickSortNonR(arr, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
7.小区间优化
#include <stdio.h>
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
void InsertSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[i+1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
int QuickSort(int arr[], int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin, right = end;
if ((end - begin + 1) <= 10)
{
InsertSort(arr+begin, end - begin + 1);
}
else
{
//三数取中找key
int midi = GetMidNumi(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
//挪数据
while (right > left)
{
while (right > left && arr[right] >= arr[keyi])
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[keyi]);
keyi = left;
QuickSort(arr, begin, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
}
int main()
{
int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8, 20, 0};
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
QuickSort(arr, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
在这个代码我们做了小区间优化
小区间优化的原因:
- 在对序列不断进行快排后,此时这个序列就趋近有序,但是快排对接近有序的序列排序效率很低,最差是O(n^2),此时我们可以用插入排序,插入排序擅长趋于有序的序列排序。
- 无论是递归的快速排序还是非递归的快速排序,他们的思路都类似二叉树的前序遍历,如果每次key选的合适,那么快速排序的分割图就像满二叉树,如果我们对区间小于10的序列进行插入排序,那么我们就不用递归了,我们几乎不用对二叉树的最后2~3层递归,这将近减少了70%的递归量。
8.时间空间复杂度和稳定性
1.时间复杂度:O(nlogn)
我们以每一层为视角来看,每一层快排都相当于把初始的待排序序列遍历了一遍,遍历一遍时间复杂度为O(n),然后一共有logn层,所以整个的时间复杂度为O(nlogn)
2.空间复杂度:O(n)
因为整个过程中,我们都在初始序列上操作,而初始序列的大小就是空间复杂度的大小,为n
3.稳定性:不稳定
当一个序列有多个相同值时,它们的相对位置会被快排打乱。
9.三路划分
1.思路
三路划分是为了解决快排中存在大量重复数据,三路划分我们需要定义key,left,cur和right四个变量,key表示三数取中后,初始序列的首元素,left指向初始序列的第一个元素,cur指向初始序列的第二个元素, right指向初始序列的最后的元素。
left,cur,right的变化情况如下:
- 当arr[cur] < key时,交换arr[left]和arr[cur],left++, cur++
- 当arr[cur] > key时,交换arr[cur]和arr[right],right--。注意此时cur位置不变,我们要继续判断cur指向元素和arr[keyi]的大小情况
- 当arr[cur] = key时,cur++。
直到cur>right为止
2.代码
#include <stdio.h>
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
int midi = (left + right) / 2;
if (arr[left] > arr[midi])
{
if (arr[midi] > arr[right])
{
return midi;
}
else
{
if (arr[right] > arr[left])
{
return left;
}
else
{
return right;
}
}
}
else //arr[left] < arr[midi]
{
if (arr[left] > arr[right])
{
return left;
}
else
{
if (arr[right] > arr[midi])
{
return midi;
}
else
{
return right;
}
}
}
}
void QuickSort(int arr[], int left, int right)
{
if (left >= right)
{
return;
}
int midi = GetMidNumi(arr, left, right);
Swap(&arr[left], &arr[midi]);
int begin = left;
int end = right;
int keyi = left, key = arr[left];
int cur = left + 1;
while (cur <= right)
{
if (arr[cur] < key)
{
Swap(&arr[cur], &arr[left]);
cur++;
left++;
}
else if (arr[cur] > key)
{
Swap(&arr[cur], &arr[right]);
right--;
}
else
{
cur++;
}
}
//[begin,left-1][left,right][cur,end]
QuickSort(arr, begin, left - 1);
QuickSort(arr, cur, end);
}
int main()
{
int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8, 11, 1, 1, 1, 1, 2, 2};
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
QuickSort(arr, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}