一、简介
快速排序是基于 分治法 思想的一种排序算法,由于排序效率在同为 O(N*logN) 的几种排序方法中效率较高,因此经常被采用。
该方法的基本思想是:
- 从数列
arr[left,right]
中取出一个数作为 基准数。(在此以 pivot 标记基准所在的下标) - 分区。将小于或等于
arr[pivot]
的数全放到它的左边,大于arr[pivot]
的数全放到它的右边。由此形成以arr[pivot]
为分界线的左右区间,[left,pivot)全是小于等于arr[pivot]
的数,(pivot,right] 全是大于arr[pivot]
的数。 - 再对左右区间重复第二步,直到各区间只有一个数。
二、分区过程
假设要对 [ left , right ] 指定范围的数列arr 进行升序排序。根据快速排序的基本思想,首先需要选取一个基准 arr[pivot]
,在此假定选取 arr[left] 为基准。
2.1 两头法
基本思路:
- 首先,从 right 起始,向左遍历,找到第一个小于
arr[pivot]
的数,即为arr[right]
。 - 然后,从 left 起始, 向右遍历,找到第一个大于
arr[pivot]
的数,即为arr[left]
。 - 交换这两个元素的值
swap(arr[right],arr[left])
。 - 重复上述步骤,直至
left < right
不成立。 - 此时,以 left 为界限的左边,将全是小于等于
arr[pivot]
的元素;右边则全是大于arr[pivot]
的元素。而arr[pivot]
还在 left 的左边,所以此时,只要将arr[pivot]
和arr[left]
交换,就能使得原始序列以arr[pivot]
为界限。
上述 查找/交换 的过程,是为了将小于 arr[pivot] 的值通过交换,交换到左边;以及将大于 arr[pivot] 的值通过交换,交换到右边。
举例说明:
以序列 {7,8,4,5,1,9,6} 为例,进行升序排序。下图是一次 partition 过程。
经过一次 partition 之后,原始序列将被分为以基准(7)为界限的两个区间。
partition 的完整代码:
int partition1(int left,int right)
{
int pivot = left; // 选择 arr[left] 为基准
while(left < right)
{
while(left < right && arr[right] >= arr[pivot])
{
right--;
}
while(left < right && arr[left] <= arr[pivot])
{
left++;
}
if(left < right)
{
swap(arr[left],arr[right]); // 将小数交换到左,大数交换到右。
}
}
swap(arr[left],arr[pivot]); // 将基准放到合适位置。
return left;
}
2.2 快慢指针
基本思路:
- 快指针由右向左遍历,每找到一个大于
arr[pivot]
的值,就交换到 slow 指定的位置,然后 slow 前移一位。slow 的含义是下次找到满足条件的值 val 时,val 该交换到 slow 指定的位置。 - 当遍历完一遍之后,所有满足条件的元素,都会被交换到 slow 的右边。而此时,
arr[pivot]
还在slow的左边;因此,接下来只要将arr[pivot]
与arr[slow]
交换位置,就可以使得原始序列以基准为分界线,分为两个区间。
举例说明:
以序列 {7,8,4,5,1,9,6} 为例,进行升序排序。下图是一次 partition 过程。
经过一次 partition 之后,原始序列将被分为以基准(7)为界限的两个区间。
partition 的完整代码:
int partition2(int left, int right)
{
int pivot = left; // 选择 arr[left] 为基准
int slow = right; // 慢指针
int quick = right; // 快指针
while(quick > left)
{
// 找到大于 arr[pivot] 的元素,交换到 slow 指定的位置。
if(arr[quick] > arr[pivot])
{
swap(arr[quick],arr[slow]);
slow--; // slow 右边全是大于 arr[pivot] 的元素。
}
quick--;
}
swap(arr[slow],arr[pivot]);
return slow;
}
三、递归实现
在第二小结中,给出了两种 partition 的思路。任何一种方法,都能够将序列分为以基准为界限的两个区间,左边区间的所有元素都将小于等于基准值;右边区间的所有元素都将大于等于基准值。
再回头看看,快速排序的基本思想:
- 从数列
arr[left,right]
中取出一个数作为 基准数。(在此以 pivot 标记基准所在的下标) - 分区。将小于或等于
arr[pivot]
的数全放到它的左边,大于arr[pivot]
的数全放到它的右边。由此形成以arr[pivot]
为分界线的左右区间,[left,pivot)全是小于等于arr[pivot]
的数,(pivot,right] 全是大于arr[pivot]
的数。 - 再对左右区间重复第二步,直到各区间只有一个数。
根据快排的基本思想,每次 partition 都能将待排序序列分为两个区间。所以,快排函数需要做的就是对这两个区间,继续 partition。显然,递归是解决快排问题的一种方法。
递归实现:
#include <iostream>
#include <algorithm>
using namespace std;
int partition1(int arr[],int left,int right)
{
int pivot = left; // 选择 arr[left] 为基准
while(left < right)
{
while(left < right && arr[right] >= arr[pivot])
{
right--;
}
while(left < right && arr[left] <= arr[pivot])
{
left++;
}
if(left < right)
{
swap(arr[left],arr[right]); // 将小数交换到左,大数交换到右。
}
}
swap(arr[left],arr[pivot]); // 将基准放到合适位置。
return left;
}
int partition2(int arr[],int left, int right)
{
int pivot = left;
int slow = right; // 慢指针
int quick = right; // 快指针
while(quick > left)
{
if(arr[quick] >= arr[pivot])
{
swap(arr[quick],arr[slow]);
slow--;
}
quick--;
}
swap(arr[slow],arr[pivot]);
return slow;
}
void quick_sort(int arr[],int left,int right)
{
if(left < right)
{
int pivot = partition1(arr,left,right); // 此处可以选择不同的 partition 方法。
quick_sort(arr,left,pivot-1);
quick_sort(arr,pivot+1,right);
}
}
int main()
{
int arr[] = {7,8,4,5,1,9,6};
quick_sort(arr,0,sizeof(arr)/sizeof(int) - 1);
for(int i = 0; i < sizeof(arr)/sizeof(int); i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}