归并排序&快速排序&堆排序
Posted 算法入门
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了归并排序&快速排序&堆排序相关的知识,希望对你有一定的参考价值。
根据以往找实习的经验,一般很多公司在算法笔试的时候,第一道题目就是让你手写快熟排序再讲一遍。
可见,排序是多么基础的算法.....那么现在就复习一下归并排序、快速排序、和堆排序吧。
归并排序
归并排序应该是最容易理解的了,归并排序的结构大概是这样
void merge_sort(vector<int>& v, int l, int r) {
if (l == r) return ;
int mid = (l + r) / 2;
merge_sort(v, l, mid);
merge_sort(v, mid+1, r);
merge(v, l, r, mid);//这里合并区间
}
merge_sort()是不是挺像线段树的..就是二分区间,不断的递归,使得每次分开的两个区间都是有序的,然后再合并两个区间就好了。如何使区间有序?
当区间是一个数的时候就是有序的啊,递归到最底层,回溯的时候再合并两个只有一位数的区间,这样就实现了排序。
这里要注意一点:merge()合并函数就是合并两个有序的序列,得需要额外的空间开销,合并的方法是从后往前扫,比较a,b两个序列的大小,如果按照从小到大排序的话,那就是把先在新序列的末段index放a,b序列中较大的,然后index--,在找a,b序列中交到的放到新序列的index位置.....其实合并两个有序的序列为一个新的有序的序列,是一道leetcode题目,具体题号忘了...
实现代码如下:
#include <bits/stdc++.h>
using namespace std;
void merge(vector<int>& v, int l, int r, int mid) {
int index = r;
vector<int> a, b;
//for (int x = l; x <= mid; ++x) a.push_back(v[x]);
//for (int x = mid+1; x <= r; ++x) b.push_back(v[x]);
a.assign(v.begin()+l, v.begin()+mid+1);
b.assign(v.begin()+mid+1, v.begin()+r+1);
int i = a.size() - 1;
int j = b.size() - 1;
while(i >= 0 && j >= 0) {
if(a[i] > b[j]) v[index--] = a[i--];
else v[index--] = b[j--];
}
while(i >= 0) v[index--] = a[i--];
while(j >= 0) v[index--] = b[j--];
}
void merge_sort(vector<int>& v, int l, int r) {
if (l == r) return ;
int mid = (l + r) / 2;
merge_sort(v, l, mid);
merge_sort(v, mid+1, r);
//merge
merge(v, l, r, mid);
}
int main() {
int n;
srand(0);
int cnt = 0;
vector<int> v;
for (int i = 0; i < 100; ++i) {
v.clear();
for (int j = 0; j < 100; ++j) {
v.push_back(rand()%1000);
}
vector<int> w;
w.assign(v.begin(), v.begin()+v.size());
merge_sort(w, 0, w.size() - 1);
sort(v.begin(), v.end());
if (w == v) cnt++;
}
printf("correct %d\n",cnt);
return 0;
}
时间复杂度O(n*logn)
空间复杂度O(n)
快速排序
快速排序和归并排序一般都是递归实现,但是快速排序递归的时候是,先进行划分。比如找到某个基准k,把小于k的放大k的左边,大于等于k的放在k的右边。 比如序列:
{6,3,7,6,2,1,4,8}
如果选6作为基准那么一次划分后得到
{4,3,1,2,6,6,7,8}
划分后会得到两个区间[0,4][5,7],那么接下来划分[0,4][5,6]区间.....依次下去最后划分区间中只有一个数的时候结束。
代码实现如下:
void quicksort(vector<int>&v,int l,int r){
if(r<=l)return ;
int k=v[l];
int low=l;
int high=r;
while(low<high){
while(low<high&&v[high]>=k)high--;
swap(v[low],v[high]);
while(low<high&&v[low]<k)low++;
swap(v[low],v[high]);
}
quicksort(v,l,low-1);
quicksort(v,low+1,r);
}
时间复杂度O(n*logn),最坏的复杂度是O(n^2)就是当原序列是有序的时候,这时递归树就变成链了...
最优的情况下空间复杂度为O(logn):每一次都平分数组的情况 最差的情况下空间复杂度为O(n):退化为冒泡排序的情况
堆排序
堆排序,感觉很高级的样子,首先得知道堆是什么。
堆的定义如下:n个关键字序列L[n]成为堆,当且仅当该序列满足:
①L(i) <= L(2i)且L(i) <= L(2i+1) 或者 ②L(i) >= L(2i)且L(i) >= L(2i+1) 其中i属于[1, n/2]。
满足第①种情况的堆称为小根堆(小顶堆),满足第②种情况的堆称为大根堆(大顶堆)。
如下图就是一个大根堆
图片来源谷歌
你会发现左边一个数组就可以表示右边的一个二叉树了。
这里得扩展一下二叉树的知识,二叉树又分为完全二叉树(complete binary tree)和满二叉树(full binary tree) 一棵满二叉树
一棵完全二叉树
完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
本质上讲,堆排序是一种选择排序,每次都选择堆中最大的元素进行排序。只不过堆排序选择元素的方法更为先进,时间复杂度更低,效率更高。
整体的思路就是,从最后一个父节点进行调整,先比较两个子节点选最大的,然后和父节点进行比较,交换,保证父节点是最大的。
如果父节点被交换了,那么就要再次更新父节点以下的点,因为交换节点后可能存在不满足条件的子树。 如果父节点没被交换,那直接跳出函数即可。
这样一次调整结束。把堆顶的最大元素拿出来(就是与最后一个元素交换),接着对剩下的n-1个数,进行调整。具体实现见代码。
#include <bits/stdc++.h>
using namespace std;
void print(vector<int>& v) {
for (auto x : v) {
cout << x << " ";
}
cout << endl;
}
void max_heapify(vector<int>& v, int start, int end) {
//建立父节点和子节点
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { //只有当子节点在范围内才做比较
if (son + 1 <= end && v[son] < v[son + 1]) //先比较两个子节点的大小,选最大的
son++;
if (v[dad] > v[son]) //如果父节点大于子节点,调整结束
return;
else { //否则在交换父子节点,然后在进行调整
swap(v[dad], v[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(vector<int>& v, int len) {
//初始化,i从最后一个父节点调整
//print(v);
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(v, i, len - 1);
//现将第一个元素和已经排好的元素前一位做交换,再重新调整,直到排序完成
for (int i = len - 1; i > 0; i--) {
swap(v[0], v[i]);
max_heapify(v, 0, i - 1);
}
//print(v);
}
int main() {
vector<int>v;
for (int i = 10; i >= 0; --i) {
v.push_back(i);
}
heap_sort(v, v.size());
return 0;
}
空间复杂度O(1)
时间复杂度O(nlogn)
欢迎关注:)
以上是关于归并排序&快速排序&堆排序的主要内容,如果未能解决你的问题,请参考以下文章