常见的三种高级排序:快排,归并,堆排序都是Onlogn的复杂度。
快速排序
思想:
- 分治:DFS前序遍历,把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列。
- 挖坑法
1.挑选基准值 2.分割:所有比基准值小的元素放在基准值左面,大的都放到基准值右面,在这个过程结束后,基准值的排序就完成了。
复杂度
时间复杂度O(nlogn) 空间复杂度S(nlogn)
最差的情况就是每一次取到的元素就是数组中最小/最大的,也就是每次排序只能排一个数,这种情况其实就是冒泡排序了,时间复杂度O(n^2)
最好的情况是每次都选到的是中间的数则T[n] = 2T[n/2] + O(n) 时间复杂度就是O(nlogn)
//相当于DFS的前序遍历,先处理,然后对左子树进行递归,也就是进行左侧排序,再对右边排序
//分治:左边分一半,右边分一半,分别执行partition
void quickSort(vector<int>& arr, int low, int high)
{
//结束条件
if (low >= high) return;
//挖坑法处理:调用该函数,该函数会把第一个值作为基准值,并且把所有小于基准值的值都放到基准值左侧。大的放到右侧,并返回该值位置
int index = partition(arr, low, high);//挖坑法会对整个序列扫描,也就是O(n)
//每次基准值已经在正确的位置了,所以基准值不需要再排序
quickSort(arr, low, index - 1); //对基准值左侧进行排序
quickSort(arr, index + 1, high); // 对基准值右侧再排序
//T[n] = 2T[n/2] + O(n)
}
// partition(挖坑法)方法就是把传入的数组中第一个数作为基准值,然后把基准值放到正确的位置,返回这个位置
//挖坑法会对整个序列扫描,也就是O(n)
int partition(vector<int>& arr, int l, int r)
{
int x = arr[l]; // 选第一个数作为基准值,并空出该坑
while (l < r) {
// 从右向左找第一个小于基准数的数
while (l < r && arr[r] >= x) {
r--;
}
arr[l] = arr[r]; // 找到该数,放入空出来的坑中,
// 从左向右找第一个大于等于基准数的数
while (l < r && arr[l] <= x) {
l++;
}
arr[r] = arr[l];//找到该数放入之前的坑
}
//这时空出的坑的位置就是我们需要放置基准值的位置
arr[l] = x;
return l;
}
例题
- 剑指 Offer 40. 最小的k个数(快排思想)
归并排序
建立在归并操作上的一种有效排序,是分治法的典型应用
思想
分治,解决,合并(使用DFS后序遍历)
- 分治,使用后序遍历进行归并,也就是后序遍历会一组一组将俩俩排序数组合并
- 解决:排序两个有序数组:merge方法:把两个有序数组,组合成一个有序数组,类似于把两个有序链表组合成一个有序链表
- 合并:由于是后序遍历,所以他会自底向上先合并最底层,然后一层一层向上合并排序数组,这样到最顶层的时候整个数组就排序完成
复杂度
归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
// merge方法:把两个有序数组,组合成一个有序数组,类似于把两个有序链表组合成一个有序链表
// 思路:两个指针分别指向两个链表头节点,哪个小就连接哪个,然后后移该指针,继续比较当前两个指针指向的值
// 将一个数组中left到mid,与mid到right这两个数组合并
vector<int> res;
void merge(vector<int>& nums,int left, int mid, int right) {
//两个数组大小分别为mid-left+1 right-mid ,合并这两个长度的数组
int pa = left;//左vector头
int pb = mid+1;//右vector头
int curIndex = left;
while (pa <= mid && pb <= right) {
if(nums[pa] <= nums[pb]) {
res[curIndex++] = nums[pa++];
} else {
res[curIndex++] = nums[pb++];
}
}
//把剩下的数组中没放完的放进去
while (pa <= mid) res[curIndex++] = nums[pa++];
while (pb <= right) res[curIndex++] = nums[pb++];
//把排序好的数组放回nums
for (int i = left; i <= right; ++i) {
nums[i] = res[i];
}
}
//分治,使用后序遍历进行归并,也就是后序遍历会一组一组将俩俩排序数组合并,由于是后序遍历,所以他会自底向上
//先合并最底层,然后一层一层向上合并排序数组,这样到最顶层的时候整个数组就排序完成
void dfs(vector<int>& nums,int left,int right)
{
if (left >= right) return;
int mid = (left+right)/2;//首先进行分区,然后递归操作
dfs(nums,left,mid);
dfs(nums,mid+1,right);//第一次将其分为两个区间,进行合并操作
merge(nums,left,mid,right);
}
vector<int> mergeSort(vector<int>& nums) {
dfs(nums,0,nums.size()-1);
return nums;
}
测试结果
int main()
{
//随机产生N个数
int N = 200000;
srand(time(NULL)); //生成随机数种子
//放到vector中
vector<int> nums(N);
for (int i = 0; i < N; ++i) {
nums[i] = 1 + (rand() % N);
//cout << nums[i] << ",";
}
cout << endl;
//开始计时
double start, finish; /* 定义开始的时间和结束的时间 */
start = (double)clock();
//20000
//BubbleSort(nums); //2288ms
//SelectionSort(nums); //915ms
//InsertionSort(nums); //528ms
//200000
//mergeSort(nums); //93ms
//quickSort(nums, 0, nums.size()-1);//41ms
//HeapSort(nums);//53ms
finish = (double)clock();
//打印
// for (auto item : nums)
// {
// cout << item << ",";
// }
cout << (finish) - start << " ms" << endl;
}
外部排序
外部排序
https://blog.csdn.net/ailunlee/article/details/84548950
比如有100个数据排序,而我内存只能容纳10个数
1.那么我们先将100个数据分成10组,每组可以进行内部排序,比如使用归并或者快排
2.然后进行二路归并,也就是挑出两组对两个有序数组进行排序
3.两个有序数组进行排序,俩俩数据读取将数据放入内存,然后比较哪个小就将哪个输出到硬盘
4.当然这里可以使用多路归并,也就是,10路归并,每组数取第一个,这样取10个数放入内存,然后选出最小的输出到硬盘,输出的那组数再读入一个,保证内存中始终有10个数然后依次输出到硬盘。
例题
-
- 有序矩阵中第 K 小的元素(多路归并)