各类排序算法总结

排序算法一直搞不清楚,最近学习排序算法,结合相关书籍和相关网站,总结下。

什么是排序

在算法中,排序分为内部排序和外部排序。一般我们的排序程序把要排序的数据集放在内存中,一顿操作就排序好了,但是万一数据集太大,内存不够怎么办?这时候就要外部排序了,也就是把数据暂存外存(磁盘)。

外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,再分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。

所以这里我们只讨论内部排序。

内部排序如何分类?

引用一张神图:

这里我们就以打扑克牌为例,讨论非线性时间比较类排序的几种算法 :

插入排序(简单插入排序+希尔排序)

简单插入排序:大家开开心心坐成一圈,对着桌上的一副牌开始抽牌。每人轮流抽一张,每次抽到的牌我们都会思考下要插到哪,然后插进手牌中。这就是插入排序。

简单插入排序:时间复杂度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, temp, 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(nlog2n),空间复杂度O(nlog2n)。

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--;
		if (j > i)
			a[i] = a[j];//a[i]已经赋值给temp,所以直接将a[j]赋值给a[i],赋值完之后a[j],有空位
		while (i < j&&a[i] <= temp)
			i++;
		if (i < j)
			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;
}

总结

  • 对于比较类算法:堆排、快排、归并排的时间复杂度都是O(nlogn),但是空间复杂度分别为O(1)、O(n)、O(nlogn),依次增大。其他算法如插排都是简单粗暴的两层遍历,所以时间复杂度为O(n²),时间换空间了,空间复杂度就只有O(1)。另外常见算法中只有选排、堆排、快排是不稳定的。
  • 对于非比较类算法:时间和空间复杂度都是O(n+k),k是分割的份数。桶排序的效率取决于映射函数。

猜你喜欢

转载自blog.csdn.net/Protocols7/article/details/85331344