排序
冒泡排序(bubble sort)
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
冒泡排序还可以优化,比如本来要外循环6次,但是在第4次外循环的时候已经发现没有可以交换的了,那么就可以提前结束排序过程了
void bubbleSort(std::vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
bool flag = false;//提前退出冒泡循环的标志
for (int i = 0; i < n; ++i) {
flag = false;
for (int j = 0; j < n - i - 1; ++j) {
//正常,跳过
if (nums[j] <= nums[j + 1]) continue;
//不正常
std::swap(nums[j], nums[j + 1]);
flag = true;
}
if (!flag) break;
}
}
插入排序(insertion sort)
我们先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。
void insertionSort(std::vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int tmp = nums[i];
int j = i - 1;
//从i往前查找
for (; j >= 0; --j)
{
if (nums[j] > tmp)
{
nums[j + 1] = nums[j];//如果比要插入的tmp值大,就要移动到tmp后面
}
else {
break;
}
}
nums[j+1] = tmp;//此时的nums[j+1]不比tmp大,可以插入在此了. 为什么是j+1, 因为跳出for循环的时候j-1了
}
}
选择排序(selection sort)
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
void selectionSort(std::vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
for (int i = 0; i < n; ++i)
{
int minNum = nums[i];
int minNumIndex = -1;
for (int j = i + 1; j < n; ++j)
{
if (nums[j] >= minNum) continue;
minNum = nums[j];
minNumIndex = j;
}
if (minNumIndex >= 0)
{
std::swap(nums[i], nums[minNumIndex]);
}
}
}
归并排序(merge sort)
归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
因此核心思想就是:先拆 -> 不能再拆 -> 再合 -> 不能再合,这样就对应到
void mergeSort_c(std::vector<int>& nums, int left, int right)
{
if (left >= right) return;
int mid = left + (right - left) / 2;
//拆
mergeSort_c(nums, left, mid);
mergeSort_c(nums, mid+1, right);
//合
std::vector<int> tmp;
int i = left, j = mid + 1;
while (i <= mid && j <= right)
{
if (nums[i] < nums[j]) {
tmp.push_back(nums[i]);
i++;
}
else {
tmp.push_back(nums[j]);
j++;
}
}
while (i <= mid) {
tmp.push_back(nums[i]);
i++;
}
while (j <= right) {
tmp.push_back(nums[j]);
j++;
}
//tmp替换掉nums里的[left,right]区间
for (int i = 0; i < tmp.size(); ++i)
{
nums[left + i] = tmp[i];
}
}
void mergeSort(std::vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
mergeSort_c(nums, 0, n - 1);
}
快速排序(quick sort)
快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
void quickSort_c(std::vector<int>& nums, int left, int right)
{
if (left >= right) return;
int i = left;
int j = right;
int base = nums[left];
while (i < j) {
//从右向左,如果遇到了小于base的停下
//注意:一定要先从右向左扫描,因为上一次交换后nums[j]>base,nums[i]<base,
//此时待扫描区间[i,j],若先从左往右扫描,当[i,j-1]都小于base时,会在j的位置跳出while,
//此时i==j,nums[j]>base, nums[left]不能和nums[j]交换,否则不满足左边都是小于base
//同理,如果base = nums[right],则一定要先从左往右扫描,保证退出while时右边都大于base
while (nums[j] >= base && i < j) {
j--;
}
//从左向右,如果遇到了大于base的停下
while (nums[i] <= base && i < j) {
i++;
}
if (i < j) {
//此时nums[i]>base, nums[j]<base,因此对调即可实现 小于base的都在左边,大于base的都在右边
std::swap(nums[i], nums[j]);
}
/*
for (auto& i : nums) {
std::cout << i << ",";
}
std::cout << " " << i << "," << j << "\n";
*/
}
//此时i=j, 将base(nums[left]) 和 nums[i] 对调
nums[left] = nums[i];
nums[i] = base;
//递归处理 左半部分, 右半部分
quickSort_c(nums, left, i - 1);
quickSort_c(nums, i + 1, right);
}
void quickSort(std::vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
quickSort_c(nums, 0, n-1);
}
这里需要注意:一定要先从右向左扫描,因为上一次交换后nums[j]>base,nums[i]<base,此时待扫描区间[i,j],若先从左往右扫描,当[i,j-1]都小于base时,会在j的位置跳出while,此时i==j,nums[j]>base, nums[left]不能和nums[j]交换,否则不满足左边都是小于base。同理,如果base = nums[right],则一定要先从左往右扫描,保证退出while时右边都大于base。
例如:
nums = {4,5,6,3,2,1,23,18,56,88,19,32,10}, base = 4
当区间[i,j]为[2,4]时,此时对应值是{2,3,6}, 应该在nums[i]==3
的位置上跳出循环,而不是nums[i]==6
基数排序(radix sort)
核心思想是把每个数按每一位分桶
以 11 10 19 28 20 39 32 9 5 8 0 为例。
- 第一轮 : 按个位数排序
桶 | 数 |
---|---|
0 | 10,20, 0 |
1 | 11 |
2 | 32 |
3 | |
4 | |
5 | 5 |
6 | |
7 | |
8 | 28, 8 |
9 | 19, 39, 9 |
此时对应排序为 : 10,20, 0, 11, 32, 5, 28, 8, 19, 39, 9
- 第二轮 : 按十位数排序
桶 | 数 |
---|---|
0 | 0,5,8,9 |
1 | 10,11,19 |
2 | 20, 28 |
3 | 32,39 |
4 | |
5 | |
6 | |
7 | |
8 | |
9 |
此时对应排序为 : 0,5,8,9,10,11,19,20,28,32,39
因为最多只有两位,此时已完成了排序。如果有更多位,则接着进行。
// 这里为了简便,以字符串表示数字,并且假定 无负数 且 每个数字长度相同
void radixSort(vector<string>& nums) {
int n = nums.size();
if (n == 0) return;
int m = nums[0].size();
vector<int> lenVec(n);
for (int i = 0; i < n; ++i) {
lenVec[i] = i;
}
for (int len = 0; len < m; ++len) {
vector<vector<int>> radix(10);
for (int index : lenVec) {
radix[nums[index][m - 1 - len] - '0'].emplace_back(index);
}
int index = 0;
for (const auto& i : radix) {
for (auto j : i) {
lenVec[index++] = j;
}
}
}
auto copy = nums;
for (int i = 0; i < n; ++i) {
nums[i] = copy[lenVec[i]];
}
}
参考资料
极客时间 数据结构与算法之美 王铮