排序算法一直搞不清楚,最近学习排序算法,结合相关书籍和相关网站,总结下。
什么是排序
在算法中,排序分为内部排序和外部排序。一般我们的排序程序把要排序的数据集放在内存中,一顿操作就排序好了,但是万一数据集太大,内存不够怎么办?这时候就要外部排序了,也就是把数据暂存外存(磁盘)。
外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,再分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。
所以这里我们只讨论内部排序。
内部排序如何分类?
引用一张神图:
- 对于比较类算法:堆排、快排、归并排的时间复杂度都是O(nlogn),但是空间复杂度分别为O(1)、O(n)、O(nlogn),依次增大。其他算法如插排都是简单粗暴的两层遍历,所以时间复杂度为O(n²),时间换空间了,空间复杂度就只有O(1)。另外常见算法中只有选排、堆排、快排是不稳定的。
- 对于非比较类算法:时间和空间复杂度都是O(n+k),k是分割的份数。桶排序的效率取决于映射函数。
插入排序(简单插入排序+希尔排序)
简单插入排序:大家开开心心坐成一圈,对着桌上的一副牌开始抽牌。每人轮流抽一张,每次抽到的牌我们都会思考下要插到哪,然后插进手牌中。这就是插入排序。
简单插入排序:时间复杂度O(n²)。空间复杂度O(1)。
void insert_sort(int a[], int n) {
int key, k;
for (int i = 1; i < n; i++) {//抽n张牌 循环
key = a[i];//现在你抽到一张牌了,大小是key
k = i - 1;//你把牌拿在手牌最右边比较(假设手牌从左往右递增)
while (key < a[k] && k >= 0) {//你发现每张牌都比key大
a[k + 1] = a[k];//那就把这些牌后移,给key腾出位置
k--;//继续往前找
}//终于找到了一个比key小的牌了,循环结束
a[k + 1] = key;//位置也腾出来了,直接插在它右边
}
}
希尔排序:选择一个增量序列,对这个序列进行k趟排序。
总结:插入排序每次取出一个元素,插到已排序队列中,是最简单的算法之一。
选择排序(简单选择排序+堆排序)
打着打着,小明开启了上帝模式:他直接拿起牌堆,找到最小的那张,和牌堆顶一个序列的最后一张替换。循环这种操作,最后牌堆都是有序的了。这就是选择排序。
简单选择排序:时间复杂度O(n²) 空间复杂度O(1)。
void select_sort(int a[], int n) {
int i, j, min_index = 0;
for (i = 0; i < n - 1; i++) {//从牌堆进行n-1次选择,每次都选出最小的
min_index = i;//每轮最小的都是手牌最右那张
for (j = i + 1; j < n; j++) {
if (a[min_index] > a[j]) min_index = j;//min记录最小数
}//找到最小值后,与已排序数组最后一个值a[i]进行替换
std::swap(a[min_index], a[i]);
}
}
堆排序:维护一个大顶堆。注意数组的树形表示:根结点为root,左孩子即为2*root,右孩子即为2*root+1。时间复杂度O(nlog2n),空间复杂度O(1)。这是速度最快的排序,但比快排慢一点。这里可以参考为什么堆排比快排慢?
算法如下:
1、将原始序列构成一个大顶堆(builld_maxHeap():先构建完全二叉树,再对堆顶调整heapify());
2、交换堆的第一个元素(堆顶)和最后一个元素(最后一个元素就是本轮最大的);
3、无视(删去)最后一个元素,将新序列做一次调整heapify()。
4、回到第二步,直到大顶堆为空。此时的完全二叉树即为排好序的队列。
int len;
void heapify(vector<int>&a, int i) {
int lchild = 2 * i + 1, rchild = 2 * i + 2, max = i;//i为根节点,得到左右孩子的指针,假设最大为i
if (lchild<len && a[lchild]>a[max])
max = lchild;//若不存在左 右孩子,max就不变
if (rchild<len && a[rchild]>a[max])
max = rchild;//找到左孩子、右孩子、根结点这三者最大的下标为max
if (max != i) {//max没变 说明左右孩子都不存左,也就不用继续调整了
swap(a[max], a[i]);
heapify(a, max);//把堆顶和某个孩子换位了,这个三角形没问题了,但是以孩子为顶的三角形不一定是对的
//所以还需要继续调整下面的三角形,直到自顶向下的都没问题了就停止
}
}
void builld_maxHeap(vector<int> &a) {
for (int i = len / 2; i >= 0; i--) {
heapify(a, i);//对一半的结点进行堆调整即可
}
}
void heap_sort(vector<int> &a) {//把root为根节点的序列调整为一个大顶堆
len = a.size();
builld_maxHeap(a);
for (int i = a.size() - 1; i > 0; i--) {//循环n-1次
swap(a[0], a[i]);//把堆顶a[0]和队尾a[i]调换位置
len--;
heapify(a, 0);//从堆顶调整
}
}
总结:选择排序的精髓在于选择,每次从牌堆选出最大的一张牌(选择排序遍历数组比较,堆排序利用堆结构的性质每次选出最大),放到已排序数组的末尾。
交换排序(冒泡排序+快速排序)
小黑也来操作了,但是他的能力只允许交换两张牌。只见他自底向上将相邻牌两两比较,大的冒个泡,上浮一层,这样最大的就会浮到水面(牌堆顶),重复多轮,每轮都可以浮起一张最大的,这就是冒泡排序。然后他又换了玩法:以任意一张牌为基准,大于这张牌的放在上面,小于的放在下面,然后继续对上半部分和下半部分进行这种操作,这就是快速排序。
冒泡排序:时间复杂度O(n²),空间复杂度O(1)。
void bubble_sort(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]);
}
}
}
快速排序:时间复杂度O(nlogn),空间复杂度O(nlogn)。
快排是分治法的典型应用:快速排序的本质就是把基准数大的都放在基准数的右边,把比基准数小的放在基准数的左边,这样就找到了该数据在数组中的正确位置。接下来递归处理左半边和右半边即可。
int quick_sort(int a[], int left, int right){
if (left >= right) return 0;
int i = left;
int j = right;
int temp = a[left];
while (i != j){
while (i < j&&a[j] >= temp)
j--;
a[i] = a[j];//a[i]已经赋值给temp,所以直接将a[j]赋值给a[i],赋值完之后a[j],有空位
while (i < j&&a[i] <= temp)
i++;
a[j] = a[i];
}
a[i] = temp;//把基准插入,此时i与j已经相等
quick_sort(a, left, i - 1);/*递归左边*/
quick_sort(a, i + 1, right);/*递归右边*/
}
有朋友说,“5分钟内写不出快排的就别来应聘了”,吓的我赶紧看了看代码。
总结: 交换排序,时间花费在两张牌的交换上。冒泡要交换n²次,但是快排两两分割,左部的不能和右部比,所以极大的加速了算法。快排是最快的算法。
归并排序:
归并排序比较简单,是分治法的典型应用。如2路归并排序:把长度为n的序列分为两个n/2的子序列 ,对这两个序列进行归并排序,再合并这两个子序列。有点像快排,但是他们区别在于归并排序分割毫不费力,合并要两两比较着合并;快排分割的时候要用力找出一个分界点,而合并的时候毫不费力。时间复杂度O(nlog2n),空间复杂度O(n)。
int* merge(int *a, int a_len, int *b, int b_len) {
vector<int> res;
int *t1 = a, *t2 = b;
while (a_len > 0 && b_len > 0) {
if (*t1 < *t2) {
res.push_back(*t1);
t1++;
a_len--;
}
else {
res.push_back(*t2);
t2++;
b_len--;
}
}
while (a_len)
{
res.push_back(*t1);
t1++;
a_len--;
}
while (b_len) {
res.push_back(*t2);
t2++;
b_len--;
}
int *re = new int[res.size()];
if (!res.empty()) {
memcpy(re, &res[0], res.size() * sizeof(int));
}
return re;
}
int* merge_sort(int *a, int n) {
if (n < 2) return a;
int mid = n / 2;
int *t = a;
return merge(merge_sort(a, mid), mid, merge_sort(t + mid, n - mid), n - mid);
}
再看看线性时间非比较类算法,理解下 线性时间&&非比较类,也就是说不用比较两个数,就可以在线性时间内搞定排序。下面三种算法的时间&空间复杂度都是O(n+k)。其实算一种trick了,先看看计数排序。
计数排序利用了要排序的元素都是整数这个特点,利用map数据结构来方便地返回结果。算法为:
1、找出数组最大元素max;
2、把数字关系存入map中(哈希表),map[i]表明数字出现的频率;
3、i从0循环到max+1,若map[i]不为0,说明出现过,插入到数组尾部,出现几次插几次、
vector<int> counting_sort(vector<int>a, int max) {
map<int, int> bucket;
int t = 0;
for (int i = 0; i < a.size(); i++) {
if (!bucket[a[i]])
{
bucket[a[i]] = 0;
}
bucket[a[i]]++;
}
for (int i = 0; i < max + 1; i++) {
while (bucket[i] > 0)
{
a[t++] = i;
bucket[i]--;
}
}
return a;
}
桶排序
桶排序和计数排序很像,假设输入数据服从均匀分布,利用映射函数将数据分到有限数量的桶中,每个桶再分别排序。
基数排序
基数排序是一种哈希算法(如果没说错),把个位组成一个索引,进行计数排序;再把百位组成一个索引,进行计数排序……
抄的代码:
/**
* 基数排序:C++
*
* @author skywang
* @date 2014/03/15
*/
#include<iostream>
using namespace std;
/*
* 获取数组a中最大值
*
* 参数说明:
* a -- 数组
* n -- 数组长度
*/
int getMax(int a[], int n)
{
int i, max;
max = a[0];
for (i = 1; i < n; i++)
if (a[i] > max)
max = a[i];
return max;
}
/*
* 对数组按照"某个位数"进行排序(桶排序)
*
* 参数说明:
* a -- 数组
* n -- 数组长度
* exp -- 指数。对数组a按照该指数进行排序。
*
* 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
* (01) 当exp=1表示按照"个位"对数组a进行排序
* (02) 当exp=10表示按照"十位"对数组a进行排序
* (03) 当exp=100表示按照"百位"对数组a进行排序
* ...
*/
void countSort(int a[], int n, int exp)
{
int output[n]; // 存储"被排序数据"的临时数组
int i, buckets[10] = {0};
// 将数据出现的次数存储在buckets[]中
for (i = 0; i < n; i++)
buckets[ (a[i]/exp)%10 ]++;
// 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
for (i = 1; i < 10; i++)
buckets[i] += buckets[i - 1];
// 将数据存储到临时数组output[]中
for (i = n - 1; i >= 0; i--)
{
output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
buckets[ (a[i]/exp)%10 ]--;
}
// 将排序好的数据赋值给a[]
for (i = 0; i < n; i++)
a[i] = output[i];
}
/*
* 基数排序
*
* 参数说明:
* a -- 数组
* n -- 数组长度
*/
void radixSort(int a[], int n)
{
int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
int max = getMax(a, n); // 数组a中的最大值
// 从个位开始,对数组a按"指数"进行排序
for (exp = 1; max/exp > 0; exp *= 10)
countSort(a, n, exp);
}
int main()
{
int i;
int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
int ilen = (sizeof(a)) / (sizeof(a[0]));
cout << "before sort:";
for (i=0; i<ilen; i++)
cout << a[i] << " ";
cout << endl;
radixSort(a, ilen); // 基数排序
cout << "after sort:";
for (i=0; i<ilen; i++)
cout << a[i] << " ";
cout << endl;
return 0;
}