常见的排序算法
下面介绍各排序算法的思路和代码,其中快速排序和归并排序的代码可以在 leetcode. 912 排序数组 里进行测试。
快速排序(QuickSort)
快速排序从数组中随机挑一个数(叫做pivot),把比它小的数放到它左侧,把比它大的数放到它右侧,再对它左侧和右侧的子数组分别重复这个操作。
快排分三步:(1)停止条件;(2)partition函数找到pivot应该在的位置;(3)对pivot左右part分别递归。快排最关键的是partition函数,它将pivot放到该在的位置并返回这个位置。partition函数有lomuto、hoare、经典快排三种实现方式,下面分别给出这三种实现。
1. lomuto partition
把随机选出的pivot放到末尾,用j往右遍历整个数组,遍历过程中将比pivot小的元素换到前面,用i来标记已知的小元素在的区间[1, i)。等j遍历完,位置i上的元素就是第一个>=pivot的元素,把pivot换到这个位置上去。
优点:不易出错,单向遍历(适合链表的快排)
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
if(nums.empty()) return nums;
quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
return nums;
}
void quickSort(vector<int>& nums, int low, int high)
{
if(high <= low) return; //迭代终止条件:没有元素(<)或只有一个元素(=)
int p = partition(nums, low, high);
quickSort(nums, low, p - 1);
quickSort(nums, p + 1, high);
}
int partition(vector<int> &nums, int low, int high)
{
//partition的Lomuto实现方式
int pivot = low + rand() % (high - low + 1); //随机选取一个数字作为pivot。也可以直接取开头或结尾的数字,但遇到不好的case会超时
swap(nums[pivot], nums[high]); //把pivot放在结尾,会好写一点
int i = low;
for(int j = i; j < high; ++j)
{
if(nums[j] < nums[high])
{
swap(nums[i], nums[j]);
++i;
}
}
swap(nums[i], nums[high]); // [1, i)是所有小于pivot的元素,pivot在结尾,把i位置的元素跟pivot换一下就行了
return i;
}
};
2. hoare partition
把中间的元素当做pivot,用i和j分别从左侧和右侧向中间遍历数组,i记录比pivot小的,j记录比pivot大的,如果不满足条件就交换i和j的元素。注意pivot可能会被换位置,返回的j只能保证pivot在[low, j]区间内,但不一定在位置j上,所以迭代时要取到p。
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
if(nums.empty()) return nums;
quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
return nums;
}
void quickSort(vector<int>& nums, int low, int high)
{
if(high <= low) return; //迭代终止条件:没有元素(<)或只有一个元素(=)
int p = partition(nums, low, high);
quickSort(nums, low, p); //hoare partition时,locPivot位置并非一定是pivot所在的位置,pivot在[low, p]内任一位置,所以这里取到了p、不是取到p-1
quickSort(nums, p + 1, high);
}
int partition(vector<int> &nums, int low, int high)
{
//partition的hoare实现方式
int p = low + (high - low) / 2;
int pivot = nums[p]; //hoare partition时,一开始选的pivot可能会被换位置,所以要跟pivot的值比较,不能跟nums这个位置上的的数字比较
int i = low, j = high;
while(true)
{
while(nums[i] < pivot) ++i;
while(nums[j] > pivot) --j;
if(i >= j) return j;
swap(nums[i], nums[j]);
++i;
--j;
}
}
};
3. 经典快排
对hoare partition的改进,减少交换次数。
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
if(nums.empty()) return nums;
quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
return nums;
}
void quickSort(vector<int> & nums, int low, int high)
{
if(low >= high) return;
int locPivot = partition(nums, low, high);
quickSort(nums, low, locPivot); // 取到了locPivot
quickSort(nums, locPivot + 1, high);
}
int partition(vector<int> & nums, int low, int high)
{
int p = rand() % (high - low + 1) + low;
int pivot = nums[p];
swap(nums[p], nums[low]);
while(low < high)
{
while(low < high && nums[high] >= pivot) --high; //注意>=
nums[low] = nums[high];
while(low < high && nums[low] <= pivot) ++low; //注意<=
nums[high] = nums[low];
}
nums[low] = pivot;
return low;
}
};
归并排序(MergeSort)
归并排序将数组分成两个子数组,分别对两个子数组排序,然后合并这两个有序数组。
归并排序有递归和迭代两种写法,下面分别给出这两种实现方式。归并排序最重要的合并两个有序数组的merge函数。
1. 递归(Recursive):自上而下
class Solution {
public:
// 归并排序方法一:递归recursive,自上而下
// 分三步:(1)终止条件;(2)把当前序列平分成两个子序列,分别进行递归排序;(3)用双指针对两个递增子序列的结果进行merge
vector<int> sortArray(vector<int>& nums) {
if(nums.empty()) return nums;
vector<int> tmp(nums.size(), 0); //需要一个额外的辅助空间
mergeSortRecursive(nums, 0, nums.size() - 1, tmp);
return nums;
}
// 通用的merge函数:对nums[low,...,mid]和nums[mid+1,...,high]两个递增数组进行合并
void merge(vector<int> & nums, int low, int mid, int high, vector<int> & tmp)
{
int i = low, j = mid + 1, k = low; // i是子序列1的起始位置,j是子序列2的起始位置,k是暂存数组tmp的索引
while(i <= mid && j <= high)
{
if(nums[i] <= nums[j]) tmp[k++] = nums[i++];
else tmp[k++] = nums[j++];
}
while(i <= mid) tmp[k++] = nums[i++];
while(j <= high) tmp[k++] = nums[j++];
for(int p = low; p <= high; ++p)
{
nums[p] = tmp[p];
}
}
void mergeSortRecursive(vector<int>& nums, int low, int high, vector<int> & tmp)
{
if(low >= high) return ; //没有元素,或只有一个元素
int mid = low + (high - low) / 2;
mergeSortRecursive(nums, low, mid, tmp);
mergeSortRecursive(nums, mid + 1, high, tmp);
merge(nums, low, mid, high, tmp);
}
};
2. 迭代(Iterative):自下而上
class Solution {
public:
// 归并排序方法二:迭代iterative,自下而上
// 分两步:(1)对子序列长度len从1开始进行2倍递增;(2)根据当前len计算得到相邻两个子序列的low、mid、high,进行merge;
vector<int> sortArray(vector<int>& nums) {
if(nums.empty()) return nums;
vector<int> tmp(nums.size(), 0); //需要一个额外的辅助空间
int n = nums.size();
for(int len = 1; len < n; len *= 2) //len表示两两merge的子序列中,一个子序列的长度。从1,2,4,8这样两倍变化
{
for(int low = 0; low < n; low += 2*len) //low表示每两两merge的子序列中,第一个子序列的起始位置
{
int mid = min(low + len - 1, n - 1); // mid表示每两两merge的子序列中,第一个子序列的结束位置
int high = min(low + 2 * len - 1, n - 1); //high表示每两两merge的子序列中,第二个子序列的结束位置
merge(nums, low, mid, high, tmp);
}
}
return nums;
}
// 通用的merge函数:对nums[low,...,mid]和nums[mid+1,...,high]两个递增数组进行合并
void merge(vector<int> & nums, int low, int mid, int high, vector<int> & tmp)
{
int i = low, j = mid + 1, k = low; // i是子序列1的起始位置,j是子序列2的起始位置,k是暂存数组tmp的索引
while(i <= mid && j <= high)
{
if(nums[i] <= nums[j]) tmp[k++] = nums[i++];
else tmp[k++] = nums[j++];
}
while(i <= mid) tmp[k++] = nums[i++];
while(j <= high) tmp[k++] = nums[j++];
for(int p = low; p <= high; ++p)
{
nums[p] = tmp[p];
}
}
};
插入排序(InsertionSort)
插入排序时,i从左到右遍历整个数组,将nums[i]插入到[0, i-1]的已排序区间里该在的位置。插入方法是倒着交换过去。
void insertionSort(vector<int> & nums)
{
for(int i = 0; i < nums.size(); ++i)
{
for(int j = i; j > 0; --j)
{
if(nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
}
}
}
冒泡排序(BubbleSort)
从左到右遍历数组,对相邻的两个元素进行比较,看是否左<=右,如果不满足就让它俩互换。一次冒泡会让最大的放到最后面,重复 n 次,就完成了 n 个数据的排序工作。当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
一次冒泡:
n次冒泡:
void bubbleSort(vector<int> & nums)
{
bool swapped;
for(int i = 1; i < nums.size(); ++i)
{
swapped = false;
for(int j = 1; j < nums.size() - i + 1; ++j)
{
if(nums[j] < nums[j - 1])
{
swap(nums[j], nums[j - 1]);
swapped = true;
}
}
if(!swapped)
{
break;
}
}
}
选择排序(SelectionSort)
选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
void selectionSort(vector<int> &nums)
{
int mid;
for(int i = 0; i < n - 1; ++i)
{
mid = i;
for(int j = i + 1; j < n; ++j)
{
if (nums[j] < nums[mid])
{
mid = j;
}
}
swap(nums[mid], nums[i]);
}
}
K-th element相关题目
leetcode 215. 数组中的第K个最大元素
快速选择:快速选择是快速排序算法的一种变形应用,通常用来在未排序的数组中寻找第k小/第k大的元素。快速选择并不递归访问双边,而是只递归进入一边的元素中继续寻找,这降低了平均时间复杂度,从O(nlogn)至O(n),不过最坏情况仍然是O(n^2)。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 快速选择:二分查找 + 通过快排的partition函数来缩小查找区间
int n = nums.size();
int l = 0, r = n - 1, mid;
k = n - k; //第k大,是从右往左第k个,所以是从左往右第n-k个
while(l <= r)
{
mid = partition(nums, l, r); //从nums的l到r里随机选一个数,将它排到自己应该在的位置mid上,[l, mid - 1]都是比它小的,[mid + 1, r]都是比它大的
if(mid == k) return nums[k];
else if(mid > k) r = mid - 1;
else l = mid + 1;
}
return r;
}
int partition(vector<int>& nums, int l, int r)
{
int p = l + rand() % (r - l + 1); //随机选一个数作为pivot
int pivot = nums[p];
swap(nums[p], nums[r]); //把pivot放到末尾比较方便
int i = l;
for(int j = i; j < r; ++j)
{
if(nums[j] < pivot)
{
swap(nums[i], nums[j]);
++i;
}
}
swap(nums[i], nums[r]);
return i;
}
};
leetcode 347. 前 K 个高频元素
桶排序:将n个元素通过hash分配到k个桶,使得每个桶内大约有5~15个元素,对各个桶内的元素通过插入排序或者递归桶排进行排序后,拼接合并在一起。平均时间复杂度O(n+k),适合分布均匀数组的排序。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// 桶排序的桶是vector,因为各个桶是有序的。普通的桶可以是unordered_map,访问快。
// 本题中,先用桶记录各数字的出现频率(这一步不是桶排序里的步骤);再通过桶排序对频率进行排序,具体做法是,按频率对数字进行分桶,因为题目要求频率k里的数字可以按任意顺序返回,所以桶内数字就不需要再进行插入排序了。Time O(n), Space O(n)
unordered_map<int, int> count; // 数字:频率
int maxCount = 0; //记录下最大的频率,作为后续桶排序的分桶数
for(int i = 0; i < nums.size(); ++i)
{
++count[nums[i]];
maxCount = max(maxCount, count[nums[i]]);
}
// 桶排序
vector<vector<int>> bucket(maxCount + 1); // 频率:[数字1, 数字2]。对各数字按出现频率分桶,加1是为了使下标和频率对应起来,都是[1, maxCount]
for(auto & ele : count)
{
bucket[ele.second].push_back(ele.first);
}
vector<int> res;
for(int j = maxCount; j >= 1 && res.size() < k; --j)
{
for(auto &num : bucket[j]) res.push_back(num);
}
return res;
}
};
本文的配图摘自极客时间APP的《数据结构与算法之美》课程插图。