文章目录
以下所有的swap()函数,函数定义为
void swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
// 也可以用异或,但不能传入同一个变量,可以是不同变量相同值
void swap(int& a, int& b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
1 冒泡排序 O ( N 2 ) O(N^2) O(N2)(与数据状况无关)
最基础、最简单的排序,没啥好说的
#incldue <vector>
void bubbleSort(std::vector<int>& arr)
{
for (int i = 0; i < arr.size() - 1; i++) {
for (int j = 0; j < arr.size() - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
// 相邻元素两两对比
swap(arr[j], arr[j+1]);
}
}
}
}
2 选择排序 O ( N 2 ) O(N^2) O(N2)(与数据状况无关)
基本思想是每次从待排序的元素中选择最小(或最大)的元素,放到已排序序列的末尾。选择排序的主要步骤如下:
- 1.遍历待排序序列,设定当前位置为最小值的位置。
- 2.从当前位置开始,依次比较当前元素与后面的元素,找到最小的元素,记录其位置。
- 3.将最小元素与当前位置的元素进行交换。
- 4.重复步骤2和步骤3,直到遍历完整个序列。
#include<vector>
void selectionSort(std::vector<int>& arr)
{
int len = arr.size();
if (arr.empty() || len < 2) return;
for (int i = 0; i < len -1; i++){
// len -1 位置不用再循环了,因为最后剩一个必然是最值
int minIndex = i; // 在i ~ len-1 上寻找最小值的下标
for (int j = i; j < len - 1; j++){
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr[minIndex], arr[i]);
}
}
3 插入排序 O ( N 2 ) O(N^2) O(N2)(与数据状况相关)
原理类似于对扑克牌手牌进行排序的过程。将未排序的元素逐个插入到已排序部分的合适位置。
逻辑顺序为先做到0~0有序, 然后0~1有序, 0~2 … 0~n-1有序
算法流程为:
- 1.从第一个元素开始,该元素可以认为已经被排序。
- 2.从未排序部分取第一个值,插入到已排序部分的适当位置。这个位置的选取方式为:把当前未排序值与左邻值对比,如果更小则交换位置。直到遇到边界或不比左邻值小则结束。(内循环:找适当位置插入)
- 3.重复步骤2,直到所有元素都被插入到已排序序列中。(外循环:遍历每个未排序的数)
这个不像冒泡排序和选择排序是固定操作(数据状况无关)。插入排序中,如果给的就是有序的,那就外层循环每次比一下就完成了所以会是O(N), 但我们说时间复杂度都是 worst case 所以还是 O ( N 2 ) O(N^2) O(N2)
#include <vector>
void insertionSort(std::vector<int>& arr)
{
int len = arr.size();
if (arr.empty() || len < 2) return;
// 0~0已经有序了
for (int i = 1; i < len; i++){
// 0~i做到有序
// 把当前值(j+1)对比左邻值(j),比他小则交换,不满足循环条件则说明找到合适位置了
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){
swap(arr[j+1], arr[j]);
}
}
}
在内层循环中,我们将当前元素 arr[j + 1] 与其左邻元素 arr[j] 进行比较,更小则换位,直到找到合适的插入位置。
循环结束(找到合适的位置)条件为:达到左边界 or 当前值比左邻值大
继续往下看建议先掌握 二分查找和基础递归
4 归并排序 O ( N l o g N ) O(NlogN) O(NlogN)(数据状况无关)
它的核心思想是将待排序的序列不断划分成更小的子序列,直到每个子序列只有一个元素,然后再将这些子序列两两合并,直到最终整个序列有序。
下面是归并排序的一般步骤:
- 分割:将待排序的序列从中间位置分割成两个子序列,不断递归地将每个子序列继续分割,直到每个子序列只剩下一个元素。
- 合并:将两个有序的子序列合并成一个有序序列。从两个子序列的第一个元素开始比较,将较小的元素放入临时数组中,并将对应子序列的索引向后移动一位,直到其中一个子序列的元素全部放入临时数组中。然后将另一个子序列的剩余元素直接放入临时数组中。
- 重复合并:重复进行合并操作,直到所有子序列都合并为一个有序序列。
始终都是 O(NlogN) 的时间复杂度,与数据无关。虽然相对前面的冒泡、选择、插入排序更快,但是空间复杂度为O(N)
下图对于这个过程的描述是非常准确的,
#include <vector>
// 第二阶段:merge阶段时,L~M 和 M~R 之间的数必然有序
void merge(std::vector<int>& arr, int L, int M, int R)
{
std::vector help(R - L + 1);
int i = 0; // 临时数组的起始索引
int p1 = L; // 左半部分的起始索引
int p2 = M + 1; // 右半部分的起始索引
//外排序,把两个子数组中的元素从小到大放到help数组
while (p1 <= M && p2 <= R){
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 如果左边部分还有剩的,全部依次加到help的末尾
while (p1 <= M){
help[i++] = arr[p1++];
}
// 同理如果右边还有剩的
while (p2 <= R){
help[i++] = arr[p2++];
}
// 结束,把临时数组的内容覆盖到原数组中
for (i = 0; i < R - L + 1; i++){
arr[L + i] = help[i]; // 注意arr从L位置开始写入的
}
}
// 务必先看这个函数 这算是第一阶段拆分
void mergeSort(std::vector<int>& arr, int L, int R)
{
if (L == R) return; // 拆分完毕,不能再拆了
int mid = L + ((R - L) >> 1);
mergeSort(arr, L, mid);
mergeSort(arr, mid+1, R);
merge(arr, L, mid, R); // 执行到这一步的条件是 L == mid 且 mid+1 == R 即左右子树都已二分到只有一个元素
}
时间复杂度(参考2.1 master公式)
- T ( N ) = 2 ⋅ T ( N 2 ) + O ( N ) T(N) = 2·T(\frac{N}{2})+O(N) T(N)=2⋅T(2N)+O(N)
- a = 2; b = 2; d = 1
- l o g a b = 1 = d log_ab=1=d logab=1=d,所以时间复杂度为 O ( N ⋅ l o g N ) O(N·logN) O(N⋅logN)
空间复杂度: O ( N ) O(N) O(N) 。因为每次merge的时候开辟一块空间,大小为N
4.1 归并排序的扩展(求数组小和)
题目:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:[1,3,4,2,5]
1左边比1小的数,无;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
要求时间复杂度O(NlogN),空间复杂度O(N)
如果每个位置的元素都遍历一遍左边所有元素,找出比它小的数,求和,很简单就能得到结果,时间复杂度为 O ( N 2 ) O(N^2) O(N2)。
转变思维:不是找左边有多少比当前位置的数更小的数,然后求和,而是统计有多少个比当前位置的数更大的数,当前数就该累加多少次,而这个过程在归并排序中的 外排序中就可以做到
具体讲解视频精准空降,比看文字可好多了
#include <iostream>
#include <vector>
long long mergeAndCount(std::vector<int>& arr, int left, int mid, int right)
{
std::vector<int> temp(right - left + 1); // 临时数组用于存储合并后的结果
int i = 0; // 临时数组的起始索引
int p1 = left; // 左半部分的起始索引
int p2 = mid + 1; // 右半部分的起始索引
long long count = 0; // 记录小和的累加和
while (p1 <= mid && p2 <= right) {
if (arr[p1] <= arr[p2]) {
// 重点:如果arr[p1]比arr[p2]小,则arr[p1]比p2后面所有数都小!
count += arr[p1] * (right - p2 + 1);
temp[i++] = arr[p1++];
} else {
temp[i++] = arr[p2++];
}
}
while (p1 <= mid) temp[i++] = arr[p1++];
while (p2 <= right) temp[i++] = arr[p2++];
// 将临时数组的结果复制回原数组
for (i = 0; i < R - L + 1; i++) {
arr[left + i] = temp[i];
}
return count;
}
long long mergeSortAndCount(std::vector<int>& arr, int left, int right)
{
if (left >= right) {
return 0; // 单个元素无小和
}
int mid = left + (right - left) / 2;
long long leftCount = mergeSortAndCount(arr, left, mid); // 左半部分的小和
long long rightCount = mergeSortAndCount(arr, mid + 1, right); // 右半部分的小和
long long mergeCount = mergeAndCount(arr, left, mid, right); // 合并过程中的小和
return leftCount + rightCount + mergeCount; // 总的小和
}
long long calculateSmallSum(std::vector<int>& arr)
{
if (arr.empty()) return 0; // 空数组无小和
int left = 0;
int right = arr.size() - 1;
return mergeSortAndCount(arr, left, right);
}
int main()
{
std::vector<int> arr = {
3, 1, 4, 2, 5};
long long smallSum = calculateSmallSum(arr);
std::cout << "Small Sum: " << smallSum << std::endl;
std::cin.get();
return 0;
}
5 快速排序
快排 3.0 (1.0 和 2.0 参考别处)