常用排序算法

以下是一些最基本的排序算法。虽然在 C++ 里可以通过 std::sort() 快速排序,而且刷题时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解出由这些排序算法引申出来的题目。

快速排序

采用左闭右开的二分写法,代码模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void quick_sort(vector<int> &nums, int l, int r) {
//特殊情况,没有数或只有一个数,无需排序,直接返回
if (l + 1 >= r)
return;
//取区间左端点值为排序对象,目标是比它大的都在右边,比它小的都在左边
int first = l, last = r - 1, key = nums[first];
while (first < last) {
//寻找比key小的,往前调
while(first < last && nums[last] >= key)
last--;
nums[first] = nums[last];//因为nums[first]的值在key中,不会丢失
//比key大的,往后调
while (first < last && nums[first] <= key)
first++;
nums[last] = nums[first];
}
nums[first] = key; //key来到了属于它的位置
//继续排左半区间和右半区间
quick_sort(nums, l, first);
quick_sort(nums, first + 1, r);
}

归并排序

归并排序主要是采用分而治之的思想,将数组不断分解成多个小数组,然后从排序好的小数组中按大小挑出数填充结果数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void merge_sort(vector<int> &nums, int l, int r, vector<int> &temp) {
if (l + 1 >= r)
return;
int m = (l + r) / 2;
//分解
merge_sort(nums, l, m, temp);
merge_sort(nums, m, r, temp);
//归并原理
int p = l, q = m, i = l;
//分别从两个小数组开头开始,先挑小的放进结果数组
while (p < m || q < r) {
if (q >= r || (p < m && nums[p] <= nums[q]))
temp[i++] = nums[p++];
else
temp[i++] = nums[q++];
}
//临时的数组赋回原数组
for (i = l; i < r; i++)
nums[i] = temp[i];
}

插入排序

先排好小的元素。

1
2
3
4
5
6
void insertion_sort(vector<int> &nums, int n) {
for (int i = 0; i < n; i++) {
for (int j = i; j > 0 && nums[j] < nums[j-1]; j--)
swap(nums[j], nums[j-1]);
}
}

冒泡排序

先排好大的元素。

1
2
3
4
5
6
7
8
void bubble_sort(vector<int> &nums, int n) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-1-i; j++) {
if (a[j] < a[j+1])
swap(nums[j], nums[j+1]);
}
}
}

选择排序

选定第一个元素为最小元,然后向后比较找真正的最小元,与之交换。

1
2
3
4
5
6
7
8
9
10
11
void selection_sort(vector<int> &nums, int n) {
int min;
for (int i = 0; i < n-1; i++) {
min = i;
for (int j = i+1; j < n; j++) {
if (nums[j] < nums[min])
min = j;
}
swap(nums[min], nums[i]);
}
}

快速选择

数组中第 k 大的元素

给定整数数组 nums 和整数 k,请返回数组中第 k 大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

快速选择一般用于求解 k-th Element 问题,可以在 O(n) 时间复杂度,O(1) 空间复杂度完成求解工作。快速选择的实现和快速排序相似,首先选定数组第一个值为基准值,将小于它的放在左边,大于它的放在右边,然后根据情况判断继续搜索左区间还是右区间。

解释手稿

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int findKthLargest(vector<int>& nums, int k) {
int l = 0, r = nums.size() - 1, target = nums.size() - k;
while (l < r) {
int mid = quickSelection(nums, l, r);
if (mid == target)
return nums[mid];
if (mid < target)
l = mid + 1;
else
r = mid - 1;
}
return nums[l];
}
// 辅函数 - 快速选择,目标是把数组按大小切成两半,返回的是分界点
int quickSelection(vector<int>& nums, int l, int r) {
int i = l + 1, j = r;
while (true) {
while (i < r && nums[i] <= nums[l])
i++;
while (l < j && nums[j] >= nums[l])
j--;
if (i >= j)
break;
swap(nums[i], nums[j]);
}
swap(nums[l], nums[j]);
return j;
}

桶排序

前 k 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

顾名思义,桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数(或其它属性),然后对桶进行排序。针对样例1来说,我们先通过桶排序得到四个桶 [1,2,3,4],它们的值分别为 [4,2,1,1],表示每个数字出现的次数。

紧接着,我们对桶的频次进行排序,前 k 大个桶即是前 k 个频繁的数。这里我们可以使用各种
排序算法,甚至可以再进行一次桶排序,把每个旧桶根据频次放在不同的新桶内。针对样例来说,因为目前最大的频次是 4,我们建立 [1,2,3,4] 四个新桶,它们分别放入的旧桶为 [[3,4],[2],[],[1]],表示不同数字出现的频率。最后,我们从后往前遍历,直到找到 k 个旧桶。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> counts;
int max_count = 0;
for (const int & num : nums) {
counts[num]++;
max_count = max(max_count, counts[num]);
}
vector<vector<int>> buckets(max_count + 1);
for (const auto & p : counts)
buckets[p.second].push_back(p.first);
vector<int> ans;
for (int i = max_count; i >= 0; i--) {
for (const int & num : buckets[i])
ans.push_back(num);
if (ans.size() == k)
break;
}
return ans;
}

练习

根据字符出现频率排序

给定一个字符串 s,根据字符出现的频率对其进行降序排序。一个字符出现的频率是它出现在字符串中的次数。

返回已排序的字符串。如果有多个答案,返回其中任何一个。

示例 1:

输入: s = “tree”
输出: “eert”

示例 2:

输入: s = “cccaaa”
输出: “cccaaa”

示例 3:

输入: s = “Aabb”
输出: “bbAa”

即桶排序。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
string frequencySort(string s) {
unordered_map<char, int> counts;
int max_count = 0;
for (const char & a : s) {
counts[a]++;
max_count = max(max_count, counts[a]);
}
vector<vector<char>> buckets(max_count + 1);
for (const auto & p : counts)
buckets[p.second].push_back(p.first);
string ans;
for (int i = max_count; i >= 0; i--) {
for (const int & a : buckets[i]) {
for (int j = 0; j < i; j++)
ans.push_back(a);
}
}
return ans;
}

颜色分类

给定一个包含红色、白色和蓝色共 n 个元素的数组 nums,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库的sort函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]

桶排序。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void sortColors(vector<int>& nums) {
unordered_map<int, int> counts;
int max_count = 0;
for (const int & num : nums)
counts[num]++;
int i = 0;
for (i = 0; i < counts[0]; i++)
nums[i] = 0;
for ( ; i < counts[1] + counts[0]; i++)
nums[i] = 1;
for ( ; i < counts[2] + counts[1] + counts[0]; i++)
nums[i] = 2;
return;
}