十大排序总结之美

排序算法说明

(1)排序的定义:对一序列对象根据某个关键字进行排序;

输入:n个数:a1,a2,a3,…,an
输出:n个数的排列:a1’,a2’,a3’,…,an’,使得a1’<=a2’<=a3’<=…<=an’。

再讲的形象点就是排排坐,调座位,高的站在后面,矮的站在前面咯。

(3)对于评述算法优劣术语的说明

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

内排序:所有排序操作都在内存中完成;
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

时间复杂度: 一个算法执行所耗费的时间。
空间复杂度: 运行完一个程序所需内存的大小。

关于时间空间复杂度的更多了解请戳这里,或是看书程杰大大编写的《大话数据结构》还是很赞的,通俗易懂。

(4)排序算法图片总结(图片来源于网络):

排序对比:

图片名词解释:
n: 数据规模
k:“桶”的个数
In-place: 占用常数内存,不占用额外内存
Out-place: 占用额外内存

排序分类:

1.冒泡排序(Bubble Sort)

好的,开始总结第一个排序算法,冒泡排序。我想对于它每个学过C语言的都会了解的吧,这可能是很多人接触的第一个排序算法。

(1)算法描述

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

(2)算法描述和实现

具体算法描述如下:

<1>.比较相邻的元素。如果第一个比第二个大,就交换它们两个;

<2>.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

<3>.针对所有的元素重复以上的步骤,除了最后一个;

<4>.重复步骤1~3,直到排序完成。

#include<iostream>
using namespace std;

//冒泡排序
void bubbleSort(int a[],int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n-1-i; j++)
		{
			if (a[j] > a[j+1])	//从小到大排序,改为小于号就是从大到小。
			{
				int tmp;
				tmp = a[j+1];
				a[j+1] = a[j];
				a[j] = tmp;
			}
		}
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	bubbleSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cin.get();
	return 0;
}

改进冒泡排序: 设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

#include<iostream>
using namespace std;

//改进版冒泡排序
void bubbleSort(int a[],int n)
{
	bool flag = false;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n-1-i; j++)
		{
			if (a[j] > a[j+1])	//从小到大排序,改为小于号就是从大到小。
			{
				int tmp;
				tmp = a[j+1];
				a[j+1] = a[j];
				a[j] = tmp;
				flag = true;
			}
		}
		if (!flag)	//当经过一轮比较以后,没有元素进行交换,就表明已经排好序,不需要在进行。
		{
			break;
		}
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	bubbleSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cin.get();
	return 0;
}

传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。

改进后的算法实现为:

#include<iostream>
using namespace std;

//进一步改进的冒泡排序
void bubbleSort(int a[],int n)
{
	int low = 0;
	int high = n - 1;
	while (low < high)
	{
		for (int i = low; i < high; i++)    //正向冒泡找到最大
		{
			if (a[i] > a[i + 1])
			{
				int tmp;
				tmp = a[i];
				a[i] = a[i+1];
				a[i + 1] = tmp;
			}
		}
		--high;
		for (int j = high; j > low; j--)    //反向冒泡找到最小
		{
			if (a[j] < a[j - 1])
			{
				int tmp;
				tmp = a[j];
				a[j] = a[j - 1];
				a[j - 1] = tmp;
			}
		}
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	bubbleSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cin.get();
	return 0;
}

冒泡排序动图演示:

三种方法耗时对比:

由图可以看出改进后的冒泡排序明显的时间复杂度更低,耗时更短了。

(4)算法分析

最佳情况:T(n) = O(n)

当输入的数据已经是正序时(都已经是正序了,为毛何必还排序呢….)

最差情况:T(n) = O(n2)

当输入的数据是反序时(卧槽,我直接反序不就完了….)

平均情况:T(n) = O(n2)

2.选择排序(Selection Sort)

表现最稳定的排序算法之一(这个稳定不是指算法层面上的稳定哈,相信聪明的你能明白我说的意思2333),因为无论什么数据进去都是O(n2)的时间复杂度…..所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

(1)算法简介

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

(2)算法描述和实现

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

<1>.初始状态:无序区为R[1..n],有序区为空;

<2>.第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

<3>.n-1趟结束,数组有序化了。

#include<iostream>
using namespace std;

//选择排序
void selectionSort(int a[],int n)
{
	int tmp;
	int mixindex;
	for (int i = 0; i < n - 1; i++)
	{
		mixindex = i;
		for (int j = i + 1; j < n; j++)
		{
			if (a[mixindex] > a[j])
			{
				mixindex = j;
			}
		}
		tmp = a[i];
		a[i] = a[mixindex];
		a[mixindex] = tmp;
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	selectionSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cin.get();
	return 0;
}

选择排序动图演示:

https://www.2cto.com/uploadfile/Collfiles/20160918/20160918092144584.gif

(3)算法分析

最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)

3.插入排序(Insertion Sort)

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。当然,如果你说你打扑克牌摸牌的时候从来不按牌的大小整理牌,那估计这辈子你对插入排序的算法都不会产生任何兴趣了…..

(1)算法简介

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

(2)算法描述和实现

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

<1>.从第一个元素开始,该元素可以认为已经被排序;

<2>.取出下一个元素,在已经排序的元素序列中从后向前扫描;

<3>.如果该元素(已排序)大于新元素,将该元素移到下一位置;

<4>.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

<5>.将新元素插入到该位置后;

<6>.重复步骤2~5。

#include<iostream>
using namespace std;

//插入排序
void insertionSort(int a[],int n)
{
	for (int i = 1; i < n; i++) 
	{
		int key = a[i];
		int j = i - 1;
		while (j >= 0 && a[j] > key)
		{
			a[j + 1] = a[j];
			j--;
		}
		a[j + 1] = key;
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	insertionSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	cin.get();
	return 0;
}

改进插入排序: 查找插入位置时使用二分查找的方式

#include<iostream>
using namespace std;

//改进版插入排序
void insertionSort(int a[],int n)
{
	for (int i = 1; i < n; i++) 
	{
		int key = a[i], left = 0, right = i - 1;
		while (left <= right) 
		{
			int middle = (left + right) / 2;
			if (key < a[middle]) 
			{
				right = middle - 1;
			}
			else 
			{
				left = middle + 1;
			}
		}
		for (int j = i - 1; j >= left; j--) 
		{
			a[j + 1] = a[j];
		}
		a[left] = key;
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	insertionSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	cin.get();
	return 0;
}

改进前后对比:

插入排序动图演示:

这里写图片描述

(3)算法分析

最佳情况:输入数组按升序排列。T(n) = O(n) 最坏情况:输入数组按降序排列。T(n) = O(n2) 平均情况:T(n) = O(n2)

4.希尔排序(Shell Sort)

1959年Shell发明; 第一个突破O(n^2)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

(1)算法简介

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。

(2)算法描述和实现

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

<1>. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

<2>.按增量序列个数k,对序列进行k 趟排序;

<3>.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

#include<iostream>
#include<cmath>
using namespace std;

//希尔排序
void shellSort(int a[],int n)
{
	for (int gap = floor(n/ 2); gap > 0; gap = floor(gap / 2)) 
	{
		// 注意:这里和动图演示的不一样,动图是分组执行,实际操作是多个分组交替执行
		for (int i = gap; i < n; i++) 
		{
			int j = i;
			int current = a[i];
			while (j - gap >= 0 && current < a[j - gap]) 
			{
				a[j] = a[j - gap];
				j = j - gap;
			}
			a[j] = current;
		}
	}
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	shellSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	cin.get();
	return 0;
}

希尔排序动图演示:

为了进一步理解希尔排序举例说明希尔排序法过程:

以一个整数序列为例来说明{12,45,90,1,34,87,-3,822,23,-222,32},该组序列包含N=11个数。不少已有的说明中通常举例10个数,这里说明一下,排序算法与序列元素个数无关!
首先声明一个参数:增量gap。gap初始值设置为N/2。缩小方式一般为gap=gap/2.
第一步,gap=N/2=5,每间隔5个元素取一个数,组成一组,一共得到5组:

对每组使用插入排序算法,得到每组的有序数列:

至此,数列已变为:

第二步,缩小gap,gap=gap/2=2,每间隔2取一个数,组成一组,共两组:

同理,分别使用插入排序法,得到每组的有序数列:

至此,数列已变为:

第三步,进一步缩小gap,gap=gap/2=1,此时只有一组,直接使用插入排序法,玩完成排序,图略。


shell排序C++代码实现

步进方式我这里用的是:gap=gap/3

#include<iostream>
 
using namespace std;
const int INCRGAP = 3;
void shellSort(int a[],int len)
{
    int insertNum = 0;
    unsigned gap = len/INCRGAP + 1; // 步长初始化,注意如果当len<INCRGAP时,gap为0,所以为了保证进入循环,gap至少为1!!!
    while(gap) // while gap>=1
    {
        for (unsigned i = gap; i < len; ++i) // 分组,在每个子序列中进行插入排序
        {
            insertNum = a[i];//将当前的元素值先存起来方便后面插入
            unsigned j = i;
            while (j >= gap && insertNum < a[j-gap])//寻找插入位置
            {
                a[j] = a[j - gap];
                j -= gap;
            }
            a[j] = insertNum;
        }
        gap = gap/INCRGAP;
    }
}
int main()
{
    int array[11] = {2, 1, 4, 3, 11, 6, 5, 7, 8, 10, 15};
    shellSort(array, 11);
    for(auto it: array)
    {
        cout<<it<<endl;
    }
    return 0;
}

注意:

如果步进方式为gap=gap/2的话不用考虑下面这个问题:

那么gap初始化的时候就不用+1了,代码如下:

#include<iostream>
 
using namespace std;
const int INCRGAP = 2;
void shellSort(int a[],int len)
{
    int insertNum = 0;
    unsigned gap = len/INCRGAP; // 步长初始化
    while(gap) // while gap>=1
    {
        for (unsigned i = gap; i < len; ++i) // 分组,在每个子序列中进行插入排序
        {
            insertNum = a[i];//将当前的元素值先存起来方便后面插入
            unsigned j = i;
            while (j >= gap && insertNum < a[j-gap])//寻找插入位置
            {
                a[j] = a[j - gap];
                j -= gap;
            }
            a[j] = insertNum;
        }
        gap = gap/INCRGAP;
    }
}
int main()
{
    int array[11] = {2, 1, 4, 3, 11, 6, 5, 7, 8, 10, 15};
    shellSort(array, 11);
    for(auto it: array)
    {
        cout<<it<<endl;
    }
    return 0;
}

(3)算法分析

最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog n)

5.归并排序(Merge Sort)

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。

(1)算法简介

 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

(2)算法描述和实现

具体算法描述如下:

<1>.把长度为n的输入序列分成两个长度为n/2的子序列;

<2>.对这两个子序列分别采用归并排序;

<3>.将两个排序好的子序列合并成一个最终的排序序列。

#include<iostream>
#include<cmath>
using namespace std;

//归并排序
//合并两个序列
void mergeArray(int arr[], int first, int mid, int last, int temp[])
{
	int i = first;
	int j = mid + 1;
	int m = mid;
	int n = last;
	int k = 0;
	while (i <= m && j <= n)
	{
		if (arr[i] <= arr[j])
			temp[k++] = arr[i++];
		else
			temp[k++] = arr[j++];
	}
	while (i <= m)
		temp[k++] = arr[i++];
	while (j <= n)
		temp[k++] = arr[j++];
	for (i = 0; i < k; i++)
		arr[first + i] = temp[i];
}
void mySort(int arr[], int first, int last, int temp[])
{
	if (first < last)
	{
		int mid = (first + last) / 2;
		mySort(arr, first, mid, temp);
		mySort(arr, mid + 1, last, temp);
		mergeArray(arr, first, mid, last, temp);
	}
}
bool mergeSort(int arr[], int len)
{
	int*p = new int[len];
	if (NULL == p)
		return false;
	mySort(arr, 0, len - 1, p);
	delete[] p;
	return true;
}

int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	mergeSort(a, n);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	cin.get();
	return 0;
}

归并排序动图演示:

这里写图片描述

归并排序图解:

(3)算法分析

最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)

6.快速排序(Quick Sort)

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一了。

(1)算法简介

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

(2)算法描述和实现

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

<1>.从数列中挑出一个元素,称为 “基准”(pivot);

<2>.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

<3>.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

#include<iostream>
#include<cmath>
using namespace std;

//快速排序
void quickSort(int a[], int low, int high)
{
	if (low < high)  //判断是否满足排序条件,递归的终止条件
	{
		int i = low, j = high;   //把待排序数组元素的第一个和最后一个下标分别赋值给i,j,使用i,j进行排序;
		int x = a[low];    //将待排序数组的第一个元素作为哨兵,将数组划分为大于哨兵以及小于哨兵的两部分                                   
		while (i < j)
		{
			while (i < j && a[j] >= x) j--;  //从最右侧元素开始,如果比哨兵大,那么它的位置就正确,然后判断前一个元素,直到不满足条件
			if (i < j) a[i++] = a[j];   //把不满足位次条件的那个元素值赋值给第一个元素,(也即是哨兵元素,此时哨兵已经保存在x中,不会丢失)并把i的加1
			while (i < j && a[i] <= x) i++; //换成左侧下标为i的元素开始与哨兵比较大小,比其小,那么它所处的位置就正确,然后判断后一个,直到不满足条件
			if (i < j) a[j--] = a[i];  //把不满足位次条件的那个元素值赋值给下标为j的元素,(下标为j的元素已经保存到前面,不会丢失)并把j的加1
		}
		a[i] = x;   //完成一次排序,把哨兵赋值到下标为i的位置,即前面的都比它小,后面的都比它大
		quickSort(a, low, i - 1);  //递归进行哨兵前后两部分元素排序 , low,high的值不发生变化,i处于中间
		quickSort(a, i + 1, high);
	}
}


int main()
{
	int a[] = { 9,8,7,6,2,3,4,5 };
	int n = sizeof(a)/sizeof(int);
	quickSort(a, 0, n-1);
	for (int i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	cin.get();
	return 0;
}

快速排序动图演示:

快速排序

快速排序图解:

(3)算法分析

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)

7.堆排序(Heap Sort)

堆排序可以说是一种利用堆的概念来排序的选择排序。

(1)算法简介

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

(2)算法描述和实现

具体算法描述如下:

<1>.将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;

<2>.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];

<3>.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

#include <iostream>
using namespace std;

/*
 * (最大)堆的向下调整算法
 *
 * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
 *     end   -- 截至范围(一般为数组中最后一个元素的索引)
 */
void maxHeapDown(int* a, int start, int end)
{
	int c = start;            // 当前(current)节点的位置
	int l = 2 * c + 1;        // 左(left)孩子的位置
	int tmp = a[c];            // 当前(current)节点的大小
	for (; l <= end; c = l, l = 2 * l + 1)
	{
		// "l"是左孩子,"l+1"是右孩子
		if (l < end && a[l] < a[l + 1])
			l++;        // 左右两孩子中选择较大者,即m_heap[l+1]
		if (tmp >= a[l])
			break;        // 调整结束
		else            // 交换值
		{
			a[c] = a[l];
			a[l] = tmp;
		}
	}
}

/*
 * 堆排序(从小到大)
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     n -- 数组的长度
 */
void heapSortAsc(int* a, int n)
{
	int i, tmp;

	// 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
	for (i = n / 2 - 1; i >= 0; i--)
		maxHeapDown(a, i, n - 1);

	// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
	for (i = n - 1; i > 0; i--)
	{
		// 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。
		tmp = a[0];
		a[0] = a[i];
		a[i] = tmp;
		// 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。
		// 即,保证a[i-1]是a[0...i-1]中的最大值。
		maxHeapDown(a, 0, i - 1);
	}
}

/*
 * (最小)堆的向下调整算法
 *
 * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
 *     end   -- 截至范围(一般为数组中最后一个元素的索引)
 */
void minHeapDown(int* a, int start, int end)
{
	int c = start;            // 当前(current)节点的位置
	int l = 2 * c + 1;        // 左(left)孩子的位置
	int tmp = a[c];            // 当前(current)节点的大小
	for (; l <= end; c = l, l = 2 * l + 1)
	{
		// "l"是左孩子,"l+1"是右孩子
		if (l < end && a[l] > a[l + 1])
			l++;        // 左右两孩子中选择较小者
		if (tmp <= a[l])
			break;        // 调整结束
		else            // 交换值
		{
			a[c] = a[l];
			a[l] = tmp;
		}
	}
}

/*
 * 堆排序(从大到小)
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     n -- 数组的长度
 */
void heapSortDesc(int* a, int n)
{
	int i, tmp;

	// 从(n/2-1) --> 0逐次遍历每。遍历之后,得到的数组实际上是一个最小堆。
	for (i = n / 2 - 1; i >= 0; i--)
		minHeapDown(a, i, n - 1);

	// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
	for (i = n - 1; i > 0; i--)
	{
		// 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最小的。
		tmp = a[0];
		a[0] = a[i];
		a[i] = tmp;
		// 调整a[0...i-1],使得a[0...i-1]仍然是一个最小堆。
		// 即,保证a[i-1]是a[0...i-1]中的最小值。
		minHeapDown(a, 0, i - 1);
	}
}

int main()
{
	int i;
	int a[] = { 20,30,90,40,70,110,60,10,100,50,80 };
	int ilen = (sizeof(a)) / (sizeof(a[0]));

	cout << "before sort:";
	for (i = 0; i < ilen; i++)
		cout << a[i] << " ";
	cout << endl;

	heapSortAsc(a, ilen);            // 升序排列
	//heapSortDesc(a, ilen);        // 降序排列

	cout << "after  sort:";
	for (i = 0; i < ilen; i++)
		cout << a[i] << " ";
	cout << endl;
	cin.get();
	return 0;
}

堆排序动图演示:

这里写图片描述

图解堆排序

(3)算法分析

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)

8.计数排序(Counting Sort)

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

(1)算法简介

计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

(2)算法描述和实现

具体算法描述如下:

<1>. 找出待排序的数组中最大和最小的元素;

<2>. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

<3>. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

<4>. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

void CountSort(vector<int>& a, int size) 
{  
  int max = a[0]; 
  int min = a[0]; 
  for (int i = 0; i < size; ++i) 
  { 
    if (a[i] > max) 
    { 
      max = a[i]; 
    } 
    if (a[i] < min) 
    { 
      min = a[i]; 
    } 
  } 
  int range = max - min + 1; 
  vector<int> count(range ,0);  
  //统计每个数出现的次数 
  for (int i = 0; i < size; ++i)    
  { 
    count[a[i]-min]++; 
  } 
  //写回到原数组 
  int index = 0; 
  for (int i = 0; i < range; ++i)  //从开辟的数组中读取,开辟的数组大小为range 
  { 
    while (count[i]--) 
    { 
      a[index++] = i + min; 
    } 
  } 
} 

计数排序动图演示:

这里写图片描述

计数排序图解:

(3)算法分析

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)

9.桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

(1)算法简介

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排

(2)算法描述和实现

具体算法描述如下:

<1>.设置一个定量的数组当作空桶;

<2>.遍历输入数据,并且把数据一个一个放到对应的桶里去;

<3>.对每个不是空的桶进行排序;

<4>.从不是空的桶里把排好序的数据拼接起来。

#include <iostream>
#include <cstring>
using namespace std;

/*
 * 桶排序
 *
 * 参数说明:
 *     a -- 待排序数组
 *     n -- 数组a的长度
 *     max -- 数组a中最大值的范围
 */
void bucketSort(int* a, int n, int max)
{
    int i, j;
    int *buckets;

    if (a==NULL || n<1 || max<1)
        return ;

    // 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
    if ((buckets = new int[max])==NULL)
        return ;
    memset(buckets, 0, max*sizeof(int));

    // 1. 计数
    for(i = 0; i < n; i++) 
        buckets[a[i]]++; 

    // 2. 排序
    for (i = 0, j = 0; i < max; i++) 
        while( (buckets[i]--) >0 )
            a[j++] = i;

    delete[] buckets;
}


int main()
{
    int i;
    int a[] = {8,2,3,4,3,6,6,3,9};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    bucketSort(a, ilen, 10); // 桶排序

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

桶排序图解:

(3)算法分析

 桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)

10.基数排序(Radix Sort)

基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;

(1)算法简介

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

(2)算法描述和实现

具体算法描述如下:

<1>.取得数组中的最大数,并取得位数;

<2>.arr为原始数组,从最低位开始取每个位组成radix数组;

<3>.对radix进行计数排序(利用计数排序适用于小范围数的特点);

#include <bits/stdc++.h>
using namespace std;
int ArrLength(int Arr[]) //测出int数组的长度 
{
    int i = 0; 
    while (Arr[i])
        i ++;
    return i;
}
    /**
     * 递归,找出数组最大的值
     *
     * @param arrays 数组
     * @param L      左边界,第一个数
     * @param R      右边界,数组的长度
     * @return
     */
int findMax(int arrays[],int L,int R)
{
	//如果该数组只有一个数,那么最大的就是该数组第一个值了
    if (L == R)
	return arrays[L];
    else
    {
		int a = arrays[L];
	    int b = findMax(arrays, L + 1, R);//找出整体的最大值
		if (a > b)
		    return a;
		else
		{
		    return b;
		}
    }
}
void radixSort(int arrays[])
{
    int max = findMax(arrays, 0,ArrLength(arrays) - 1);
    //需要遍历的次数由数组最大值的位数来决定
    for (int i = 1; max / i > 0; i = i * 10)
    {
		int buckets[ArrLength(arrays)][10];
		//获取每一位数字(个、十、百、千位...分配到桶子里)
		memset(buckets,0,sizeof(buckets));//【重点】一定要memset 一定要memset 否则会内存泄漏(Segmentation fault) 
		for (int j = 0; j < ArrLength(arrays); j++)
	    {
			int num = (arrays[j] / i) % 10;
			//将其放入桶子里
		    buckets[j][num] = arrays[j];
	    }
	    //回收桶子里的元素
		int k = 0;
		//有10个桶子
		for (int j = 0; j < 10; j++)
		{
			 //对每个桶子里的元素进行回收
		    for (int l = 0; l < ArrLength(arrays); l++)
		    {
		    	//如果桶子里面有元素就回收(数据初始化会为0)
				if (buckets[l][j] != 0)
				{
				    arrays[k++] = buckets[l][j];
				}
		    }
	  	}
    }
}
int main()
{
    int arrays[100] = {73,22,93,43,55,14,28,65,39,81};
    radixSort(arrays);
    for (int i = 0;i <= ArrLength(arrays) - 1 ;i ++)
		cout << arrays[i] << " ";
    return 0;
}

基数排序LSD动图演示:

这里写图片描述

(3)算法分析

最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)

基数排序有两种方法:

MSD 从高位开始进行排序 LSD 从低位开始进行排序

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

基数排序:根据键值的每位数字来分配桶 计数排序:每个桶只存储单一键值 桶排序:每个桶存储一定范围的数值

 

注:本人水平有限,如有发现错误或者有更好的实现欢迎指出。

参考资料:

https://www.2cto.com/kf/201609/548586.html

https://www.cnblogs.com/onepixel/articles/7674659.html

https://www.jb51.net/article/113482.htm

https://www.cnblogs.com/zkfopen/p/11191905.html

https://www.cnblogs.com/skywang12345/p/3602162.html#a1

https://www.cnblogs.com/LJA001162/p/11217276.html

发布了109 篇原创文章 · 获赞 179 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_44350205/article/details/104904993