快速排序:快速排序算法通过多次比较和交换来实现排序
基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
适用情况:数据量大,要求的效率也高,应用广
时间复杂度和空间复杂度和稳定性:O(nlogn)~O(n2) ,O(logn)~O(n) 不稳定 (这里提到空间复杂度是因为其他排序都是O(1))
快排有三种实现方法,且都比较重要。
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版本
2. 挖坑法
3. 前后指针版本
hoare版本
1、定义两个指针begin(指向首元素)和end(指向尾元素),找一个基准值key(一般三数取中法),置于序列的第一个元素,也就是begin的位置。
2、end从后往前走找比基准值小的元素(找到后也停下),begin从前往后走找比基准值key大的元素(找到后停下),
然后,交换arr[begin]和arr[end],依次循环操作。
3、当begin与end相遇,将array[begin]或array[end]与基准值交换。
详解:(建议和代码一起看)
代码:
//获取基准值:三数取中法:起始, 中间, 结束
int GetMid(int* arr, int begin, int end)
{
int mid = begin + (end - begin) / 2;
if (arr[begin] > arr[mid])
{
if (arr[mid] > arr[end])
return mid;
else if (arr[begin] > arr[end])
return end;
else
return begin;
}
else
{
if (arr[mid] < arr[end])
return mid;
else if (arr[begin] < arr[end])
return end;
else
return begin;
}
}
//返回基准值位置
int Partion(int* arr, int begin, int end)
{
//获取基准值的位置
int mid = GetMid(arr, begin, end);
//把基准值放到起始位置
Swap(arr, begin, mid);
int key = arr[begin];
int start = begin;
while (begin < end)
{
//从后向前先找小于基准值的位置
while (begin < end && arr[end] >= key)
{
--end;
}
//从前向后再找大于基准值的位置
while (begin < end && arr[begin] <= key)
{
++begin;
}
Swap(arr, begin, end);
}
//交换相遇位置的数据和基准值
Swap(arr, start, begin);
return begin;
}
//数据有序时,没有优化可能会导致栈溢出(代码优化,重新找基准值)
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
//div:一次划分之后,基准值的位置
int div = Partion(arr, begin, end);
//左右两部分进行快速排序
QuickSort(arr, begin, div - 1);
QuickSort(arr, div + 1, end);
}
运行结果:
挖坑法
挖坑法和hoare版本很像
1、定义begin和end分别指向数据的第一个元素和最后一个元素,找一个基准值key(一般三数取中法),置array[begin]的位置上的值为基准值,并为第一个坑。
2、end从后往前走,找比key小的值,找到之后,将array[end]赋值给array[begin],填充begin位置的坑,此时end位置为一个新的坑
3、begin从前往后走,找比key大的值,找到之后,将array[begin]赋值给array[end],填充end位置的坑,此时begin位置为一个坑
4、此类方法依次填坑,当begin和end相遇,则循环结束,将key的值填最后一个坑。
详解:(建议和代码一起看)
代码:
//获取基准值:三数取中法:起始, 中间, 结束
int GetMid(int* arr, int begin, int end)
{
int mid = begin + (end - begin) / 2;
if (arr[begin] > arr[mid])
{
if (arr[mid] > arr[end])
return mid;
else if (arr[begin] > arr[end])
return end;
else
return begin;
}
else
{
if (arr[mid] < arr[end])
return mid;
else if (arr[begin] < arr[end])
return end;
else
return begin;
}
}
//挖坑法
int Partion2(int* arr, int begin, int end)
{
//获取基准值的位置
int mid = GetMid(arr, begin, end);
//把基准值放到起始位置
Swap(arr, begin, mid);
int key = arr[begin];
while (begin < end)
{
//从后向前先找小于基准值的位置
while (begin < end && arr[end] >= key)
{
--end;
}
//填坑
arr[begin] = arr[end];
//从前向后再找大于基准值的位置
while (begin < end && arr[begin] <= key)
{
++begin;
}
//填坑
arr[end] = arr[begin];
}
//相遇位置存放基准值
arr[begin] = key;
return begin;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
//div:一次划分之后,基准值的位置
int div = Partion2(arr, begin, end);
//左右两部分进行快速排序
QuickSort(arr, begin, div - 1);
QuickSort(arr, div + 1, end);
}
运行结果:
前后指针版本
1、选择一个基准值key,定义两个指针prev和cur(prev指向pPcur的前一个位置)
2、当cur标记的元素比key小时,prev和cur同时走,当cur标记的元素比key大时,只有cur继续向前走(此时prev停下来),当cur走到标记的元素比key值小时,cur停下,prev向前走一步,此时交换arr[cur]和arr[prev],然后,cur继续往前走。
3、当cur走出界了,将prev位置的值与key交换。
动画:
动画看不理解可以看图
详解:(建议和代码一起看)
代码:
//获取基准值:三数取中法:起始, 中间, 结束
int GetMid(int* arr, int begin, int end)
{
int mid = begin + (end - begin) / 2;
if (arr[begin] > arr[mid])
{
if (arr[mid] > arr[end])
return mid;
else if (arr[begin] > arr[end])
return end;
else
return begin;
}
else
{
if (arr[mid] < arr[end])
return mid;
else if (arr[begin] < arr[end])
return end;
else
return begin;
}
}
//前后指针法
int Partion3(int* arr, int begin, int end)
{
//获取基准值的位置
int mid = GetMid(arr, begin, end);
//把基准值放到起始位置
Swap(arr, begin, mid);
int key = arr[begin];
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
if (arr[cur] < key && ++prev != cur)
{
//不连续 交换数据
Swap(arr, prev, cur);
}
++cur;
}
Swap(arr, begin, prev);
return prev;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
//div:一次划分之后,基准值的位置
int div = Partion3(arr, begin, end);
//左右两部分进行快速排序
QuickSort(arr, begin, div - 1);
QuickSort(arr, div + 1, end);
}
运行结果:
非递归实现快速排序
在数据结构中,即使递归能解决的,我们也有必要掌握非递归的方法。我们知道,堆空间比栈的空间大得多,当我们在栈中递归调函数,会增加栈的开销,栈帧会多。导致栈溢出程序崩溃。而在堆上递归调用函数,即使大的递归调用也不会使程序崩溃。这在安全方面就解决了栈溢出的问题。
递归就是自上而下,再自下而上。也就满足了我们数据结构中的栈。所以这里我们可以借助栈来实现非递归的快速排序。
这里直接放代码,配合代码+注释+图,能更好理解。
//非递归快排 ,避免栈溢出风险
void QuickSortNor(int* arr, int begin, int end)
{
//定义栈
Stack st;
//初始化栈
StackInit(&st);
//先将末尾的索引入栈,
StackPush(&st, end);
//再将开头索引入栈
StackPush(&st, 0);
//只要栈不为空,就继续执行
while (!StackEmpty(&st))
{
//获得栈顶元素
int left = StackTop(&st);
//出栈
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
//获取分开后区间的中间值
int div = Partion(arr, left, right);
//上一轮key值位置的左边[left, div - 1]
if (left < div - 1)
{
StackPush(&st, div - 1);
StackPush(&st, left);
}
//上一轮key值位置的右边[div + 1, right]
if (div + 1 < right)
{
StackPush(&st, right);
StackPush(&st, div + 1);
}
}
}
运行结果:
我们可以看出,无论是递归还是非递归,我们都需要借助O(nlogn)的大小空间。(递归,栈的开销)(非递归,数据结构栈需要开空间)