十大经典排序算法-分析与C++代码实现

一、前言

二、排序(Sorting)算法概念

2.1排序算法定义

排序在百度百科中的解释为:

排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。

那么排序算法就是计算机通过某种算法将一组无序的数据按某种规则进行有序的排列,从而得到一组有序数据。

2.2相关概念与对比

  • 排序对象:数组、链表或两者皆有

  • 稳定性(Stable):假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
    假设我们有这样一组数据[7,5,2,5],然后我们来看看稳定排序算法和不稳定排序算法得出的结果:
    在这里插入图片描述

  • 时间复杂度(Time Complexity):描述运行算法所花费的时间量的计算复杂度,记做O(f(n))(记为大O表示法),本文指最坏时间复杂度
    n称为问题的规模,当n不断变化时,时间频度会跟随n规律性变化变化,用T(n)表示。若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

  • 空间复杂度((Space Complexity)):空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度(本文指最坏空间复杂度)。
    这里介绍几种常见的复杂度:
    在这里插入图片描述

  • 内外排序:在对待排序数据存放在内存中进行的排序过程,而在排序过程中需要对外部储存器进行访问时称为外排序。

  • 比较与非比较排序:比较排序过程中需要对数据关键字进行比较,反之,不需要对数据关键字进行比较的过程叫非比较排序。常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序 。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 ;计数排序、基数排序、桶排序则属于非比较排序,只要确定每个元素之前的已有的元素个数即可确定数据元素在数组中的唯一位置 。

下面搜集了一些图表记录十种排序算法的对比,方便记忆:
根据时间、空间复杂度稳定性进行比较如下:
在这里插入图片描述
上图部分名词解释: n表示数据规模, k代表“桶”的个数, In-placeOut-place表示就地排序和非就地排序,In-place不占用额外内存,Out-place需要开辟空间,占用额外内存。

根据内外比较非比较排序方式对比如下:在这里插入图片描述
在比较排序算法中,归并、插入和冒泡排序属于稳定型的,因为插入和冒泡排序都是与相邻元素的对比,相同的元素在比较过程中不会越过对方,所以稳定;归并排序是将数组划分为多个子数组,每个子数组在归并过程中互不影响,所以稳定。而非比较排序,如基数、桶、计数排序全都属于稳定排序。如下图所示:
在这里插入图片描述

三、十大经典排序算法原理、图解与代码实现

3.1冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,一层一层的将较大的元素往后移动,其现象和气泡在上升过程中慢慢变大类似,故成为冒泡排序。
1.过程图解
在这里插入图片描述
2.算法思想
(1)从第一个和第二个开始比较,如果第一个比第二个大,则交换位置,然后比较第二个和第三个,逐渐往后
(2)经过第一轮后最大的元素已经排在最后,所以重复上述操作的话第二大的则会排在倒数第二的位置。
(3)那重复上述操作n-1次即可完成排序,因为最后一次只有一个元素所以不需要比较。

3.C++代码实现

vector<int> bubble_sort(vector<int>& arr){
	for(int i=0;i<arr.size()-1;i++) {
		for(int j=0;j<arr.size()-i-1;j++) {
			if(arr[j]>arr[j+1]){
				swap(arr[j],arr[j+1]);
			}
		}
	}
	return arr;
}

4.算法分析
冒泡排序是一种简单直接暴力的排序算法,为什么说它暴力?因为每一轮比较可能多个元素移动位置,而元素位置的互换是需要消耗资源的,所以这是一种偏慢的排序算法,仅适用于对于含有较少元素的数列进行排序。

  • 稳定性:我们从代码中可以看出只有前一个元素大于后一个元素才可能交换位置,所以相同元素的相对顺序不可能改变,所以它是稳定排序
  • 比较性:因为排序时元素之间需要比较,所以是比较排序
  • 时间复杂度:因为它需要双层循环n*(n-1)),所以平均时间复杂度为O(n^2)
  • 空间复杂度:只需要常数个辅助单元,所以空间复杂度为O(1),我们把空间复杂度为O(1)也即是原地排序(in-place)
  • 记忆方法:想象成水底的气泡,从下一层一层的往上变大!

3.2选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,所以称为:选择排序。
1.过程图解
在这里插入图片描述
2.算法思想
(1)设第一个元素为比较元素,依次和后面的元素比较,比较完所有元素找到最小的元素,将它和第一个元素互换
(2)重复上述操作,我们找出第二小的元素和第二个位置的元素互换,以此类推找出剩余最小元素将它换到前面,即完成排序

3.C++代码实现

vector<int> selection_sort(vector<int>& arr){
	for(int i=0;i<arr.size()-1;i++) 
	{
		int min_index=i;  //维护一个比较过程最小值
		for(int j=i+1;j<arr.size();j++) {
			if(arr[j]<arr[min_index])  //如果遍历元素比min_index还小,则记录该最小元素下标值。
				min_index=j;
		}
		swap(arr[min_index],arr[i]);  //遍历一遍找到最小值与arr[i]交换
	}
	return arr;
}

4.算法分析

  • 比较性:因为排序时元素之间需要比较,所以是比较排序
  • 稳定性:因为存在任意位置的两个元素交换,比如[5, 8, 5, 2],第一个5会和2交换位置,所以改变了两个5原来的相对顺序,所以为不稳定排序。
  • 时间复杂度:我们看到选择排序同样是双层循环n*(n-1)),所以时间复杂度也为:O(n^2)
  • 空间复杂度:只需要常数个辅助单元,所以空间复杂度也为O(1)即In-place排序
  • 记忆方法:选择对象时先选最小的,因为小的可爱啊~

【注】选择排序冒泡排序很类似,但是选择排序每轮比较只会有一次交换,而冒泡排序会有多次交换,交换次数比冒泡排序少,就减少内存消耗,所以在数据量小的时候可以用选择排序,但实际适用的场合非常少。

3.3插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1.过程图解
在这里插入图片描述
2.算法思想
(1)从第二个元素开始和前面的元素进行比较,如果前面的元素比当前元素大,则将前面元素后移,当前元素依次往前,直到找到比它小或等于它的元素插入在其后面
(2)然后选择第三个元素,重复上述操作,进行插入
(3)依次选择到最后一个元素,插入后即完成所有排序

3.C++代码实现

vector<int> insertion_sort(vector<int>& arr){
	for(int i=1;i<arr.size();i++) {
		int current=arr[i]; //当前需要插入的元素
		int pre_index=i-1;  //与当前元素进行比较的元素
		while(pre_index>=0 && arr[pre_index]>current){
			arr[i+1]=arr[i];
			pre_index--;
		}
		arr[pre_index+1]=current;  //直到比较元素<=当前值时,把current插入到比较元素后
	}
	return arr;
}

4.算法分析

  • 比较性:排序时元素之间需要比较,所以为比较排序
  • 稳定性:从代码我们可以看出只有比较元素大于当前元素,比较元素才会往后移动,所以相同元素是不会改变相对顺序
  • 时间复杂度:插入排序同样需要两次循坏一个一个比较,故时间复杂度也为O(n^2)
  • 空间复杂度:只需要常数个辅助单元,所以空间复杂度也为O(1)
  • 记忆方法:想象成在书架中插书:先找到相应位置,将后面的书往后推,再将书插入

【注】插入排序的适用场景:一个新元素需要插入到一组已经是有序的数组中,或者是一组基本有序的数组排序。

3.4希尔排序

希尔排序(Shell Sort)是插入排序的一种又称“缩小增量(间隔)排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本,它与插入排序的不同之处在于,它会优先比较距离较远的元素,然后分组插入比较元素
1.过程图解
在这里插入图片描述
2.算法思想
希尔排序的整体思想是将固定间隔的几个元素之间排序,然后再缩小这个间隔。这样到最后数列就成为了基本有序数列,而前面我们讲过插入排序对基本有序数列排序效果较好。
(1)计算一个增量(间隔)值,把数据分组
(2)按照增量序列个数K,对K个序列进行插入排序,比如增量值为7,那么就对0起始序列0,7,14,21…个元素进行插入排序
(3)第一次排序完后,缩小增量将待排序列再次分割成若干长度的子序列,分别对各子表进行直接插入排序,比如为3,然后又重复上述第2,3步
(4)最后缩小增量至1,整个序列作为一个表来处理,表长度即为整个序列的长度。此时数列已经基本有序,最后一遍普通插入即可

3.C++代码实现

vector<int> shell_sort(vector<int>& arr){
	//希尔排序
	int len=arr.size();
	int gap=len/2;  //初始增量
	while(gap>0){
		for(int j=gap ; j-gap>=0 && arr[j]<arr[j-gap] ; j-=gap){
			std::swap(arr[j],arr[j-gap]);	
		}
		gap /= 2;  //每次增量缩小一倍,直到1为止
	}
	return arr;
}

【注】:记忆时,可以和插入排序对比记忆,插入排序每轮都是相邻元素比较(小步),而希尔排序是先间隔比较在相邻比较(先大步再小步),它是第一个突破O(n2)的排序算法,具有时代意义!

3.5归并排序

归并排序(Merge Sort)是一种利用归并操作进行排序的算法,采用分治法(Divide and Conquer)将无序数组分割成子数组分别排序,再合并成一个有序数组。
1.过程图解
在这里插入图片描述
2.算法思想
(1)利用二分法递归的将原无序数组分割为多个子数组(直到不可再分为止,即每个子数组size为1),
(2)将子数组两两有序合并,
(3)重复(2)步骤直到合并为最终的有序数组。
3.C++代码实现

vector<int> merge(vector<int> left,vector<int> right){
	//子数组合并
	vector<int> res;
	while(left.size()>0 && right.size()>0){
		//依次按照两个子数组 头元素 进行排序
		if(left.front() <= right.front()){
			res.push_back(left.front());
			vector<int>::iterator k = left.begin();  //删除left首元素
			left.erase(k);
		}
		else{
			res.push_back(right.front());
			vector<int>::iterator k = right.begin();  //删除right首元素
			right.erase(k);
		}
	}
	if(left){res.insert(res.end(), left.begin(), left.end());}  //如果left还有元素,直接加到res结尾
	if(right){res.insert(res.end(), right.begin(), right.end());}  //如果right还有元素,直接加到res结尾
	
	return res; 
}

vector<int> merge_sort(vector<int> arr){
	//递归分解再合并
	if(arr.size() == 1){
		return arr;
	}
	//二分法分解
	int mid=arr.size()/2;
	vector<int> left(arr.begin(),arr.begin()+mid+1);
	vector<int> right(arr.begin()+mid+1,arr.end());

	//递归合并
	return merge(merge_sort(left),merge_sort(right));
}

3.6快速排序

快速排序(Quick Sort)是一种基于分治思想的排序算法。通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的值均比另一部分小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
1.过程图解
在这里插入图片描述
2.算法思想
(1)从数列中挑出一个元素,称为 “基准”(pivot )
(2)数列中所有元素都和这个基准值进行比较,如果比基准值小就移到基准值的左边,如果比基准值大就移到基准值的右边
(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序,直到所有子集只剩下一个元素为止。
(4)最后一层一层返回,左子数列+基准+右子数列,就可以得到最终的有序数列。
3.C++代码实现

//根据分解子数列start和end位置,递归的进行排序
void quick_sort_recursive(vector<int> arr, int start, int end) {
    if (start >= end)
        return;
    int mid = arr[start];  //初始设置arr第一个元素为 基准值
    int left = start, right = end - 1;
   
    while (left < right) {    //在整个范围内搜寻比基准值小或大的元素,然后将左侧元素与右侧元素交换
    	//在左侧找到一个比枢纽元更大的元素:左侧元素都比基准值小
        while (arr[left] < mid && left < right)
            left++;
        //在右侧找到一个比枢纽元更小的元素:右侧元素都比基准值大
        while (arr[right] >= mid && left < right)
            right--;
        std::swap(arr[left], arr[right]); //交换元素,left为左分区,right为右分区
    }
    if (arr[left] >= arr[end])  //对于left和right之间的值,通过比较交换次序,直到left都比right小
        std::swap(arr[left], arr[end]);
    else
        left++;
    quick_sort_recursive(arr, start, left - 1);  //递归的 对左右分区进行排序,直到整体成为基本有序数列
    quick_sort_recursive(arr, left + 1, end);
}

void quick_sort(vector<int> arr, int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

【注】:通过与归并排序对比记忆,他们的排序思想都是分治思想,都是比较排序。但是它们分解和合并的策略不一样:归并是从中间直接将数列分成两个,而快排是比较后将小的放左边大的放右边,所以在合并的时候归并排序还是需要将两个数列重新再次排序,而快排则是直接合并不再需要排序,所以快排比归并排序更高效一些,就稳定性来说:归并的分区顺序不变所以虽然合并是需要重排但稳定,而快排分区依基准而定,虽然合并时不需要重排但是相对顺序被打乱而不稳定。
总结两种排序算法:思想相同策略不同,都要比较;归并像老头慢而稳定,快排像小伙子块而不稳!

3.7堆排序

3.8桶排序

3.9计数排序

3.10基数排序

参考博客:
【1】猪哥排序算法——(1)简介
【2】十大经典排序算法(附代码、动画及改进方案)

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值