常见的排序算法

2021年9月17日 21点热度 0条评论 来源: Xiao__Tian__

描述:

排序算法可谓数据结构模块中的重中之重,常见的哈希表,二叉树,搜索树/平衡树,位图等数据结构只是处理实际问题的抽象方法,实际在处理接受或生成的数据集时,排序算法显得尤其重要,排序算法家族很庞大,其中包括了冒泡排序,选择排序,插入排序,堆排序,快速排序,归并排序,基数排序,计数排序,希尔排序,箱排序,树型排序等众多算法,每种排序都有各自的特性,没有好坏之分,只有在特定的场景使用合适的排序算法才是上策,单纯的来比显得太过绝对,没有可比性。因为实际需求及各方面条件的限制使得排序算法的可选范围往往只缩小到某一种或某几种,所以要具体问题具体对待。

常见的排序算法在此列举冒泡排序,选择排序,快速排序,插入排序,堆排序,归并排序,基数排序,计数排序算法。

一:冒泡排序

算法描述:

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

算法步骤:

    1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
    2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
   3、针对所有的元素重复以上的步骤,除了最后一个。
   4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

#include<iostream>
using namespace std;

void Bubble_Sort(int* arr, size_t size)
{
	for (size_t i = 0; i < size; i++)
	{
		int temp = 0;
		for (size_t j = size - 1; j > 0; j--)
		{
			if (arr[j] < arr[j - 1])
			{
				temp = arr[j];
				arr[j] = arr[j - 1];
				arr[j - 1] = temp;
			}
		}
	}
}

▲注:该排序算法有很大的优化空间,因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量,所以整个冒泡排序过程至多需要进行n-1趟排序。以此本算法的时间复杂度还是O(n*n),也不能算是一个高效的算法。细心分析不难发现,若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。也就是说按照升序或降序排序时,若该序列本身就是一个接近有序的数列,则在某两个数据项未发生交换时退出,说明此时序列已经有序,不必再依次往后“冒泡”,提高了效率。


冒泡排序:                        


二:快速排序

算法描述:

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。

算法步骤:

    1、从数列中挑出一个元素,称为 “基准”(pivot)。
    2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
    3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

#include<iostream>
using namespace std;
#define SWAP(x,y){int tmp;tmp = x;x = y;y = tmp;}


int partition(int arr[], int left, int right)
{
	int i, j, k;
	k = arr[right];
	i = left - 1;
	for (j = left; j < right; j++)
	{
		if (arr[j] <= k)
		{
			i++;
			SWAP(arr[i], arr[j]);
		}
	}
	SWAP(arr[i + 1], arr[right]);
	return i + 1;
}


void Quick_Sort(int arr[], int left, int right)
{
	int q;
	if (left < right)
	{
		q = partition(arr, left, right);
		Quick_Sort(arr, left, q - 1);
		Quick_Sort(arr, q + 1, right);
	}
}

▲注:对于快排算法来说,其也有改进优化的空间,比如在选取第一个元素作为主元时,该主元的如何选取。对于快速排序算法的改进主要集中在三个方面:① 选取一个更好的中轴值;② 根据产生的子分区大小调整算法;③不同的划分分区的方法。

快速排序:                         

三:插入排序

算法描述:

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

算法步骤:

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

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

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

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

     5、将新元素插入到该位置中

     6、重复步骤2

#include<iostream>
using namespace std;


void Insertsort(int *arr, int size)
{
	assert(arr);
	for (int i = 1; i < size; i++)
	{
		int index = i;
		int temp = arr[index];
		int end = index - 1;
		while (end >= 0 && temp<arr[end])
		{
			arr[end + 1] = arr[end];
			end--;
		}
		arr[end + 1] = temp;
	}
}

▲注:插入排序同样也有改进优化的空间,折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。

四:选择排序

算法描述:

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

#include<iostream>
using namespace std;

void SelectionSort(int* arr, size_t size)
{
	assert(arr);
	int  min;
	for (size_t i = 0; i < size; i++)
	{
		min = i;
		for (size_t j = i + 1; j < size; j++)
			if (arr[j] < arr[min])
				min = j;
		swap(arr[i], arr[min]);
	}

}


▲注:对于简单的选择排序其改进的方法是传统的简单选择排序,每趟循环只能确定一个元素排序后的定位,所以可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。

选择排序:                           

五:堆排序

算法描述:

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

算法步骤:过程比较复杂,简单点说就三步:建堆、调堆、排序。

#include<iostream>
using namespace std;

void AdjustDown(int *arr, size_t size, int root)//向下调整
{
	size_t child = 2 * root + 1;
	while (child < size)
	{
		if (child + 1 < size && arr[child + 1] > arr[child])
		{
			++child;
		}
		if (arr[child] > arr[root])
		{
			swap(arr[child], arr[root]);
			root = child;
			child = 2 * root + 1;
		}
		else
		{
			break;
		}
	}
}


void HeapSort(int *arr, size_t size)
{
	assert(arr);
	for (int i = (size - 2) / 2; i >= 0; i--)
	{
		AdjustDown(arr, size, i);
	}
	for (size_t i = 0; i < size; ++i)
	{
		swap(arr[0], arr[size - 1 - i]);
		AdjustDown(arr, size - 1 - i, 0);
	}
}

▲注:堆排序的最明显的优势在于只需要O(1)的辅助存储空间,且明显减少了最大值的多余比较。客观的来说,堆排的性能还不错。

堆排序:                        

六:归并排序

算法描述:

    归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:

    1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

    2、设定两个指针,最初位置分别为两个已经排序序列的起始位置;
    3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
    4、重复步骤3直到某一指针达到序列尾;
    5、将另一序列剩下的所有元素直接复制到合并序列尾

#include<iostream>
using namespace std;

//将有二个有序数列a[first...mid]和a[mid...last]合并。  
void mergearray(int a[], int first, int mid, int last, int temp[])
{
	int i = first, j = mid + 1;
	int m = mid, n = last;
	int k = 0;

	while (i <= m && j <= n)
	{
		if (a[i] < a[j])
			temp[k++] = a[i++];
		else
			temp[k++] = a[j++];
	}

	while (i <= m)
		temp[k++] = a[i++];

	while (j <= n)
		temp[k++] = a[j++];

	for (i = 0; i < k; i++)
		a[first + i] = temp[i];
}


void mergesort(int a[], int first, int last, int temp[])
{
	if (first < last)
	{
		int mid = (first + last) / 2;
		mergesort(a, first, mid, temp);    //左边有序  
		mergesort(a, mid + 1, last, temp); //右边有序  
		mergearray(a, first, mid, last, temp); //再将二个有序数列合并  
	}
}


bool MergeSort(int a[], int n)
{
	int *p = new int[n];
	if (p == NULL)
		return false;
	mergesort(a, 0, n - 1, p);
	delete[] p;
	return true;
}

▲注:对于归并排序的优化最为简单的一种方法则为利用插入排序优化归并排序,在归并中利用插入排序不仅可以减少递归次数,还可以减少内存分配次数。

归并排序:                        



七:计数排序

算法描述:

     计数排序(Counting sort)是一种稳定的排序算法,和基数排序一样都是桶排序的变体。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值小于等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。

算法步骤:

     1、找出待排序的数组中最大和最小的元素;
     2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
     3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
     4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1;

void counting_sort(int A[], int length_A, int B[], int k)
{
	int C[MAX] = { 0 };//C是临时数组  
	for (int i = 1; i <= length_A; i++)
		C[A[i]]++;
	//此时C[i]包含等于i的元素个数  
	for (int i = 1; i <= k; i++)
		C[i] = C[i] + C[i - 1];
	//此时C[i]包含小于或者等于i的元素个数  
	for (int i = length_A; i >= 1; i--)//从length_A到1逆序遍历是为了保证相同元素排序后的相对顺序不改变  
	{                           //如果从1到length_A,则相同元素的相对顺序会逆序,但结果也是正确的  
		B[C[A[i]]] = A[i];
		C[A[i]]--;
	}
}


▲注:计数排序的特性是可以用在基数排序中的算法来排序数据范围很大的数组。尤其当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。但其缺点是对于字符串型数据无法处理,仅能处理数字集。

八:基数排序

算法描述:

    基数排序(Radix Sort)是对桶排序的改进和推广。唯一的区别是基数排序强调多关键字,而桶排序没有这个概念,换句话说基数排序对每一种关键字都进行桶排序,而桶排序同一个桶内排序可以任意或直接排好序。

void radixSort(int data[]) {
	int temp[10][10] = { 0 };
	int order[10] = { 0 };

	int n = 1;
	while (n <= 10) {

		int i;
		for (i = 0; i < 10; i++) {
			int lsd = ((data[i] / n) % 10);
			temp[lsd][order[lsd]] = data[i];
			order[lsd]++;
		}

		// 重新排列  
		int k = 0;
		for (i = 0; i < 10; i++) {
			if (order[i] != 0)  {
				int j;
				for (j = 0; j < order[i]; j++, k++) {
					data[k] = temp[i][j];
				}
			}
			order[i] = 0;
		}

		n *= 10;
	}
}


▲注:基数排序应用到字符串处理的倍增算法里面,这个倍增算法,要反复的进行排序。如果排序能快一点,程序就能快很多。 

※注:上图列举出了各种排序算法的时间复杂度,空间复杂度以及稳定性,并不能笼统的说哪一种算法好,遇到实际问题时根据具体需要予以选择恰当的排序算法,这才是明智之举。

    原文作者:Xiao__Tian__
    原文地址: https://blog.csdn.net/Xiao__Tian__/article/details/51600610
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。