八大经典排序算法

本文详细介绍了八种经典的排序算法:直接插入排序、希尔排序、简单选择排序、堆排序、冒泡排序、快速排序、归并排序及桶排序,并对每种算法的基本思想、操作方法、算法实现进行了阐述。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

我们这里说说八大排序就是内部排序。


当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

1.插入排序—直接插入排序(Straight Insertion Sort)

基本思想:

将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。

要点:设立哨兵,作为临时存储和判断数组边界之用。

直接插入排序示例:


如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

算法的实现:

/**********  1.插入排序—直接插入排序(Straight Insertion Sort)  ************/
void InsertSort(int a[], int n)
{
	
	int x; int j;
	for (int i = 1; i < n; i++)
	{
		j = i - 1;//要插入元素下标
		x = a[i];//哨兵

		for (; j >= 0 && x < a[j]; j--)
			a[j + 1] = a[j];

		a[j + 1] = x;//插入正确位置
	}
}

效率:

时间复杂度:O(n^2).

其他的插入排序有二分插入排序,2-路插入排序

2. 插入排序—希尔排序(Shell`s Sort)

希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序

基本思想:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

操作方法:

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

希尔排序的示例:


我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数

即:先将要排序的一组记录按某个增量dn/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

算法实现:

 

我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数

即:先将要排序的一组记录按某个增量dn/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

void ShellSort(int a[], int n)
{
	int i, j;
	int dk;//增量因子
	int temp;
	for(dk=n/2;dk>0;dk=dk/2)//增量因子减半直到为1
		for (i = dk; i < n; i++)
		{
			temp = a[i];//哨兵
			for (j = i; j >= dk; j = j - dk)//间隔为dk的序列的插入排序
			{
				if (a[j] < a[j - dk])
					a[j] = a[j - dk];//向前走到不能走为止
				else
					break;
			}
			a[j] = temp;//插入正确位置
		}
}

希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。

我们将希尔排序使用的序列h(1),h(2),、、、h(t)叫做增量序列,自要h(1)=1,任何增量序列都是可行的,但是增量序列的选取会影响运行时间。使用希尔增量时最坏的运行时间为O(N^2)。Hibbard增量的希尔排序的最坏运行时间为O(N^(3/2)),它的增量行如1、3、7、,,,、2^K-1。Sedgewick提出的增量序列的最坏的情形的运行时间为O(N^(4/3)),在实践中这些序列的运行要比Hibbard的好得多,其中最好的序列为{1,5,19,41,109,...},该序列中的项或者为9*4^i-9*2^i+1,或者是4^i-3*2^i+1。

3. 选择排序—简单选择排序(Simple Selection Sort)

基本思想:

在要排序的一组数中,选出最小(或者最大)的个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后个数)比较为止。

简单选择排序的示例:

 

操作方法:

第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

以此类推.....

第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

直到整个序列按关键码有序。

void SelectSort(int a[], int n)
{
	for (int i = 0; i < n; i++)
	{
		int k=i;
		int temp;
		
		for (int j = i+1; j < n; j++) //找到后n-i个元素中的最小的元素
		{
			if (a[j] <a[k])		//找下标就可以
				 k = j;
		}

		temp = a[k];//最小元素与第i位置元素互换 
		a[k] = a[i];
		a[i] = temp;
		
	}
}

 简单选择排序的改进——二元选择排序

简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。具体实现如下:

void SelectSort_Two(int a[], int n)
{
	for (int i = 0; i < n/2; i++)
	{
		int max = n-i-1;
		int min = i;
		int temp = 0;
		int j;

		for (int j = i + 1; j<n-i-1; j++) //找到后n-i个元素中的最小的元素
		{
			if (a[j] < a[min])		//找下标就可以
			{
				min = j; continue;
			}
			if (a[j] > a[max])
				max = j;
		}

		temp = a[min];//最小元素与第i位置元素互换 
		a[min] = a[i];
		a[i] = temp;
		temp = a[max];//最大互换
		a[max] = a[n-i-1];
		a[n-i-1] = temp;

	}
}

4. 选择排序—堆排序(Heap Sort)

堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:

堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足


时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:

(a)大顶堆序列:(96, 83,27,38,11,09)

  (b)  小顶堆序列:(12,36,24,85,47,30,53,91)



初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序

因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。


首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较小元素的进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:

再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2)筛选从第个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
                              


                              





5. 交换排序—冒泡排序(Bubble Sort)

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

 

算法的实现:

void bubbleSort(int a[], int n){  
    for(int i =0 ; i< n-1; ++i) {  
        for(int j = 0; j < n-i-1; ++j) {  
            if(a[j] > a[j+1])  
            {  
                int tmp = a[j] ; a[j] = a[j+1] ;  a[j+1] = tmp;  
            }  
        }  
    }  
} 

冒泡排序算法的改进

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:

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

改进后算法如下:

void Bubble_1 ( int r[], int n) {  
    int i= n -1;  //初始时,最后位置保持不变  
    while ( i> 0) {   
        int pos= 0; //每趟开始时,无记录交换  
        for (int j= 0; j< i; j++)  
            if (r[j]> r[j+1]) {  
                pos= j; //记录交换的位置   
                int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;  
            }   
        i= pos; //为下一趟排序作准备  
     }   
}    

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

改进后的算法实现为:

void Bubble_2 ( int r[], int n){  
    int low = 0;   
    int high= n -1; //设置变量的初始值  
    int tmp,j;  
    while (low < high) {  
        for (j= low; j< high; ++j) //正向冒泡,找到最大者  
            if (r[j]> r[j+1]) {  
                tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;  
            }   
        --high;                 //修改high值, 前移一位  
        for ( j=high; j>low; --j) //反向冒泡,找到最小者  
            if (r[j]<r[j-1]) {  
                tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;  
            }  
        ++low;                  //修改low值,后移一位  
    }   
}   

6. 交换排序—快速排序(Quick Sort)

基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:


(b)排序的全过程


算法的实现:

 递归实现:

/*************  6. 交换排序—快速排序(Quick Sort)  ****************/
void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

int partition(int a[], int low, int high)
{
	int Key = a[low]; //基准元素 
	while (low < high)//从表的两端交替地向中间扫描  
	{
		while (low<high&&a[high]>Key) high--; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端  
		swap(&a[low], &a[high]);
		while (low < high&&a[low] < Key) low++;
		swap(&a[low], &a[high]);
	}
	return low;
}
void QuickSort(int a[], int low, int high)
{
	if (low < high)
	{
		int mid = partition(a, low, high);
		QuickSort(a, low, mid - 1); //递归对低子表递归排序 
		QuickSort(a, mid + 1, high); //递归对高子表递归排序 
	}
}

分析:

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。

 
快速排序的改进

在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。算法思想如下:

/***改进快速排序*/
void qsort_improve(int r[], int low, int high, int k) {
	if (high - low > k) { //长度大于k时递归, k为指定的数  
		int pivot = partition(r, low, high); // 调用的Partition算法保持不变  
		qsort_improve(r, low, pivot - 1, k);
		qsort_improve(r, pivot + 1, high, k);
	}
}
void quickSort(int r[], int n, int k) {
	qsort_improve(r, 0, n, k);//先调用改进算法Qsort使之基本有序  

							  //再用插入排序对基本有序序列排序  
	for (int i = 1; i <= n; i++) {
		int tmp = r[i];
		int j = i - 1;
		while (tmp < r[j]) {
			r[j + 1] = r[j]; j = j - 1;
		}
		r[j + 1] = tmp;
	}

}
 

7. 归并排序(Merge Sort)


将两个的有序数列合并成一个有序数列,我们称之为"归并"。
归并排序(Merge Sort)就是利用归并思想对数列进行排序。根据具体的实现,归并排序包括"从上往下"和"从下往上"2种方式。


1. 从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。(参考下面的图片)

2. 从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
② 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。

 

下面的图片很清晰的反映了"从下往上"和"从上往下"的归并排序的区别。

将两个的有序数列合并成一个有序数列,我们称之为"归并"。
归并排序(Merge Sort)就是利用归并思想对数列进行排序。根据具体的实现,归并排序包括"从上往下"和"从下往上"2种方式。




1. 从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。(参考下面的图片)


2. 从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2; 
② 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。


 


下面的图片很清晰的反映了"从下往上"和"从上往下"的归并排序的区别。

/*********        7. 归并排序(Merge Sort)       *******/

/*
* 将一个数组中的两个相邻有序区间合并成一个
*
* 参数说明:
*     a -- 包含两个有序区间的数组
*     start -- 第1个有序区间的起始地址。
*     mid   -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。
*     end   -- 第2个有序区间的结束地址。
*/
void merge(int* a, int start, int mid, int end)    //合并函数
{
	int *tmp = new int[end - start + 1];    // tmp是汇总2个有序区的临时区域
	int i = start;            // 第1个有序区的索引
	int j = mid + 1;        // 第2个有序区的索引
	int k = 0;                // 临时区域的索引

	while (i <= mid && j <= end)
	{
		if (a[i] <= a[j])
			tmp[k++] = a[i++];
		else
			tmp[k++] = a[j++];
	}

	while (i <= mid)
		tmp[k++] = a[i++];

	while (j <= end)
		tmp[k++] = a[j++];

	// 将排序后的元素,全部都整合到数组a中。
	for (i = 0; i < k; i++)
		a[start + i] = tmp[i];

	delete[] tmp;
}

/*11111111111111
* 归并排序(从上往下)
*
* 参数说明:
*     a -- 待排序的数组
*     start -- 数组的起始地址
*     endi -- 数组的结束地址
*/
void mergeSortUp2Down(int* a, int start, int end)
{
	if (a == NULL || start >= end)
		return;

	int mid = (end + start) / 2;
	mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]
	mergeSortUp2Down(a, mid + 1, end); // 递归排序a[mid+1...end]

									   // a[start...mid] 和 a[mid...end]是两个有序空间,
									   // 将它们排序成一个有序空间a[start...end]
	merge(a, start, mid, end);
}


/*2222222222222222
* 对数组a做若干次合并:数组a的总长度为len,将它分为若干个长度为gap的子数组;
*             将"每2个相邻的子数组" 进行合并排序。
*
* 参数说明:
*     a -- 待排序的数组
*     len -- 数组的长度
*     gap -- 子数组的长度
*/
void mergeGroups(int* a, int len, int gap)
{
	int i;
	int twolen = 2 * gap;    // 两个相邻的子数组的长度

							 // 将"每2个相邻的子数组" 进行合并排序。
	for (i = 0; i + 2 * gap - 1 < len; i += (2 * gap))
	{
		merge(a, i, i + gap - 1, i + 2 * gap - 1);
	}

	// 若 i+gap-1 < len-1,则剩余一个子数组没有配对。
	// 将该子数组合并到已排序的数组中。
	if (i + gap - 1 < len - 1)
	{
		merge(a, i, i + gap - 1, len - 1);
	}
}

/*
* 归并排序(从下往上)
*
* 参数说明:
*     a -- 待排序的数组
*     len -- 数组的长度
*/
void mergeSortDown2Up(int* a, int len)
{
	int n;

	if (a == NULL || len <= 0)
		return;

	for (n = 1; n < len; n *= 2)
		mergeGroups(a, len, n);
}

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

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

	mergeSortUp2Down(a, 0, ilen - 1);        // 归并排序(从上往下)
   //mergeSortDown2Up(a, ilen);            // 归并排序(从下往上)

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

8. 桶排序

桶排序(Bucket Sort)的原理很简单,它是将数组分到有限数量的桶子里。

假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。
在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。

/************************ 8.桶排序:C++*************************/

/*
* 桶排序
*
* 参数说明:
*     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;
}
//void *memset(void *s, int ch, size_t n);
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法

9.基数排序(Radix Sort)

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序图文说明

通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。
1. 按照个位数进行排序。
2. 按照十位数进行排序。
3. 按照百位数进行排序。
排序后,数列就变成了一个有序序列。




















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值