问题引入
有一个顺序表L,假设元素类型为ElemType为整型,设计一个尽可能高效的算法,并以第一个元素为分界线(基准),将所有小于或等于基准元素的放到前面,将所有大于基准元素的放到后面。
思路:
假设数组为arr,用两个整型变量 i,j 分别指向首元素下标和尾元素下标,用base(基准)保存第一个元素。
- 选取数组最左端元素作为基准数,初始化两个指针 i 和 j 分别指向数组的两端。
- 设置一个循环,在每轮中使用 i(j)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
- 循环执行步骤 2. ,直到 i 和 j 相遇时停止,最后将基准数交换至两个子数组的分界线
int partition(int arr[], int left, int right)
{
int pivot = arr[left]; // 选取第一个元素作为基准元素
int i = left;
int j = right;
while (i < j)
{
while (i < j && arr[j] >= pivot)
j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivot)
i++;
arr[j] = arr[i];
}
arr[i] = pivot; // 上述循环跳出为i == j,此时将pivot赋给arr[i],则下标i前面的都小于等于arr[i],i后面的元素都大于arr[i]
return i; // 返回基准元素的下标
}
之所以用base保存第一个元素,因为arr[0]有可能被覆盖,等i == j跳出循环时,再将base赋给arr[i],则完成了一次分划。
快速排序
介绍
「快速排序 quick sort」是一种基于分治策略的排序算法,运行高效,应用广泛。 快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数 的元素移到其左侧,而大于基准数的元素移到其右侧。
思路
快速排序核心操作为问题引入的分划操作,进行完一次分划操作后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 ≤ 基 准数 ≤ 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。
因此我们很容易就能想到采用递归继续对左子数组和右子数组再不断进行分划操作,即可将整个数组排好序。
int partition(int arr[], int left, int right)
{
int pivot = arr[left]; // 选取第一个元素作为基准元素
int i = left;
int j = right;
while (i < j)
{
while (i < j && arr[j] >= pivot)
j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivot)
i++;
arr[j] = arr[i];
}
arr[i] = pivot; // 上述循环跳出为i == j,此时将pivot赋给arr[i],则下标i前面的都小于等于arr[i],i后面的元素都大于arr[i]
return i; // 返回基准元素的下标
}
void quick_sort(int arr[], int left, int right)
{
if (left >= right)
return;
int pos = partition(arr, left, right);
quick_sort(arr, left, pos - 1);
quick_sort(arr, pos + 1, right);
}
快速排序分析
- 时间复杂度 𝑂(𝑛 log 𝑛)、自适应排序:在平均情况下,哨兵划分的递归层数为 log 𝑛 ,每层中的总循环 数为 𝑛 ,总体使用 𝑂(𝑛 log 𝑛) 时间。在最差情况下,每轮哨兵划分操作都将长度为 𝑛 的数组划分为长 度为 0 和 𝑛−1 的两个子数组,此时递归层数达到 𝑛 层,每层中的循环数为 𝑛 ,总体使用 𝑂(𝑛2 ) 时间。
- 空间复杂度 𝑂(𝑛)、原地排序:在输入数组完全倒序的情况下,达到最差递归深度 𝑛 ,使用 𝑂(𝑛) 栈帧 空间。排序操作是在原数组上进行的,未借助额外数组。
- 非稳定排序:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。