【笔记】排序算法~

文章目录

代码模板

// 比较类排序算法

void TemplateSort(int a[], int len){
	// 【记住!】前 i-1 个元素是有序的! 
	// 第一重循环是指示需要排序的数,一般剩下最后一个就不用排序
	for(int i = 0; i < n-1; i++){
		flag = false;
		// 第二重循环是当前元素a[i]需要和那些元素进行比较
		// 如果循环内部的操作涉及到的交换元素,则是从后往前开始比对交换!
		for(int j = n-1; j>i; i--){
			// 接下来在内部写比较的函数,结果可能是交换(冒泡),覆盖(特殊的交换,边覆盖边记录位置,堆),找位置(选择排序)
			if(a[j-1] > a[j]){
				swap(a[j-1], a[j]);
				flag = true;
			}
		}
		if(flag == false) return;
	}
}

数据结构与算法复杂度

参考:

  1. 【目前见过最好的排序讲解!】【十大经典排序算法(动图演示)】
    https://www.cnblogs.com/onepixel/articles/7674659.html?tdsourcetag=s_pctim_aiomsg
  2. 改成了C++的语法
  3. 同时参考了剑指offer、数据结构课本的内容。

0、算法概述

0.1 算法分类

十种常见排序算法可以分为两大类:
**比较类排序:**通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
**非比较类排序:**不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

在这里插入图片描述

0.2 算法复杂度

在这里插入图片描述

0.3 相关概念

稳定: 如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定: 如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度: 对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度: 是指算法在计算机
内执行时所需存储空间的度量,它也是数据规模n的函数。

1. 插入排序

思想: 每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序完为止。
关键问题: 在前面已经排好序的序列中找到合适的插入位置。
方法:

  1. 直接插入排序
  2. 二分插入排序
  3. 希尔排序

1.1 直接插入排序(从后向前找到合适位置后插入)

基本思想

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法描述

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

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后; 重复步骤2~5。

动图演示

https://images2017.cnblogs.com/blog/849589/201710/849589-20171015225645277-1151100000.gif

复杂度分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

代码实现

void InsertSort(int a[], int length){
	if(a == NULL || length < 1) {
		return;
	}
	//将数据放在 1-n 的位置,循环调整元素2-n
	for(int i = 2;  i <= length; i++){
		// 在a[i]不符合升序的时候才排序
		if(a[i-1] > a[i]){
			// 哨兵元素0的设置可以避免对比j>0
			int a[0] = a[i];

			// a[i]前面的元素顺序(升序)排列的,1~ i-1 有序,i之后无序,所以从j=i开始
			// 将大于a[i]的都后移一位,知道找到第一个小于a[i]的元素,然后插到这个元素的后面,满足升序
			for(int j = i; a[j] > a[0]; j--){
				a[j] = a[j-1];
			}
			// 找到第一个大于a[i]的元素时跳出,并且将a[i]插入其后
			a[j+1] = a[0];
		}
	}
}

1.2 二分法插入排序(按二分法找到合适位置插入)

基本思想

二分法插入排序的思想和直接插入一样,只是找合适的插入位置的方式不同,这里是按二分法找到合适的位置,可以减少比较的次数。【用二分法找插入的位置】

代码实现

void InsertSort(int a[], int length){
	if(a == NULL || length < 1 ) return;
	for(int i = 2; i <= length; i++){
		if(a[i-1] > a[i]){
			// 不用哨兵,二分查找第一个小于a[i]的数字
			// 原理是目标是找到和targe相等的元素,low前面的一定是小于target的,所以high大于low的那个位置肯定是low-1的位置,也就是那个小于target的位置
			// 我们只比较了mid的元素大小,而low = mid + 1,因此不能确定low位置上是大还是小
			//范围是 1 ~ i-1,不包括i
			int low = 1, high = i-1, mid;
			while(low <= high){
				mid = ( low + high ) / 2;
				if(a[mid] > a[i]) high = mid -1;
				else low = mid + 1;
			}
			for(int j = i; j > high; j--){
				a[j] = a[j-1];
			}
			a[high + 1] = a[i];
		}
	}
}

1.3 希尔排序(增量排序,三重循环,最外层控制增量步长)

基本思想

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2

算法描述

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

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

动图演示

在这里插入图片描述

代码实现

void InsertSort(int a[], int length){
	if( !a || length <=0) return;
	//三重循环,最外层是控制步长的
	for(int dk = n/2; dk > 1; dk = dk/2)
	{
		for(int i = dk + 1; i < n-d; i--){
			if(a[i-dk] > a[i]){
				int a[0] = a[i];
				for(int j = i-dk; a[j]>a[0]; j= j - dk ){
					a[j] = a[j-dk];
				}
				a[j+dk] = a[0];
			}
		}
	}
}

2 选择排序

思想:每趟从待排序的记录序列中选择关键字最小的记录放置到已排序表的最前位置,直到全部排完。
关键问题: 在剩余的待排序记录序列中找到最小关键码记录。
方法:
–直接选择排序
–堆排序

2.1 直接选择排序(每趟记住最小数字,最后交换到最前面)

基本思想:

在要排序的一组数中,**选出最小的一个数与第一个位置的数交换;**然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法描述

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

  • 初始状态:无序区为R[1…n],有序区为空;
  • 第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个的新无序区;
  • n-1趟结束,数组有序化了。

动图演示

在这里插入图片描述

代码实现

void SelectSort(int a[], int length){
	if(!a || length <= 0 ) return;
	
	// length-1 因为最后一个数字无需排序
	for(int i = 0; i < length-1; i++){
		// 在 i~n 之间排序
		min = i; // 先假设a[i]是当前最小值
		for(int j = i+1; j < length; j++){
			if(a[j] < a[min]){
				min = j; // 只记录最小值的下标, 对应的值可以索引到
			}
		}
		// 判断一下,不是同一个位置,否则不用交换
		if(min != i) swap(a[i], a[min]);
	}
}

void swap(int a, int b){
	int temp = a;
	a = b;
	b = temp;
}

2.2 堆排序(利用堆维护本趟的最大值,堆的尾端输出)

基本思想:

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

**堆的定义:**具有n个元素的序列 (h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。

**思想:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。**所以堆排序有两个函数组成。
  一是建堆的建堆函数,
  二是反复调用建堆函数实现排序的函数。

(1)建堆:在这里插入图片描述
(2)交换,从堆中踢出最大数
在这里插入图片描述
依次类推:最后堆中剩余的最后两个结点交换,踢出一个,排序完成

算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

动图演示

在这里插入图片描述

代码实现

// 一次只调整k这个元素
void AdjustDown(ElemType a[], int k, int len){
	a[0] = a[k];
	// 注意,堆是完全二叉树,所以坐标有规律,
	// 节点i的父亲是i/2, 左右孩子是2*i, 2*i+1
	for(i = 2*k; i<=len; i*=2){
		if(i<len && a[i]<a[i+1])
			i++;  // 找到更大的孩子节点
 		if(a[0] >= a[i])
			break;  // 它比儿子们都大,所以说明已经有序的了,因为其他部分本来有序
		else{
			// 如果没有儿子大,则交换下去
			// 但是这边只是上移了孩子,因为可能会一直移动,所以就不重复赋值了,直接找到最终点,一次性写值上去
			a[k] = a[i]; // 这里覆盖了没事,a[0]还存着初始值
			k = i;  // 将指针下移
		}
	}
	a[k] = a[0];
}

void BuildHeap(Elemtype a[]){
	for ( int i=len/2; i>0; i-- ){ // 从底部往上建
		AdjustDown(a, i, len);
	}
}

void HeapSort(int a[], int len){
	BuildMaxHeap(a, len);  // 首先建堆
	for(int i = len; i>1; i--){
		swap[a[1], a[i]];	
		AdjustDown(a, 1, i-1);  // 注意!此时是从上往下调整堆,从堆顶开始调整,目前堆里面只剩下i-1个元素
	}
}

堆的元素
**删除:**删除的元素放在堆尾,然后把最后一个元素放在堆顶
**插入:**插入的元素放在堆尾,然后用adjustup调整(类似初始化的感觉)
附加,堆插入元素的代码

// 插入元素:原来的堆是有序的,所以插入的目标是只需要调整插入的元素,找到他的位置
// 其他元素只是被迫被影响了
void AdjustUp(int a[], int k, int len){
	a[0] = a[k];
	for(int i = k/2; i > 1; i = i/2){
		if(a[i] < a[0]){
			a[k] = a[i];  // 双亲节点下调
			k = i;
		}
		else break;  // 如果双亲不小于当前元素,说明已经找到位置了
	}
	a[k] = a[0];
}

3 交换排序

3.1 冒泡排序(Bubble Sort)(在一趟排序中通过交换使得最小被换到最前面)

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

算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

基本思想

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

动图演示

在这里插入图片描述

代码实现

void BubbleSort(int a[], int len){
	for(int i = 0; i < n-1; i++){
		flag = false;
		for(int j = n-1; j>i; i--){
			if(a[j-1] > a[j]){
				swap(a[j-1], a[j]);
				flag = true;
			}
		}
		if(flag == false) return;
	}
}

3.2 快速排序(通过将基准值不断交换放到正确的位置,来划分数据,直到最后一位)

基本思想

选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。

动图演示

在这里插入图片描述

代码实现

void QuickSort(int a[], int low, int high){
	if(low < high) {
		int pivot = Partition(a, low, high);
		if(pivot > start) // 相等的话,因为pivot已经在的自己的位置上了,所以剩下一个位置也是对的
			Partition(a, low, pivot-1);
		if(pivot < high)
			Partition(a, pivot+1, high);
	}
}

int Partition(int a[], int low, int high){
	int small = low-1;
	int index;
	for(int index = low; index< high; index++){
		if(a[index] < a[high]){
			small++;
			if(small != index){
				swap(a[small], a[index]);
			}
		}
	}
	small ++;
	swap(a[small], a[high]);
	return small;
}

4. 归并排序(现将数组划分到只剩一位,然后往上合并)

基本思想

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

实例

在这里插入图片描述

动图演示

在这里插入图片描述

代码实现

ElemType *b = (ElemType*)malloc((n+1)*sizeof(ElemType));
void Merge(ElemType a[], int low, int mid, int high){
	for(int index = low index<=high; index ++){
		b[index] = a[index];
	}
	// 比较B的前后两段的大小,然后合并到a
	for(int i=low, j=mid, k=low; i<=mid && j<=high; k++){
		if(b[i]<b[j]){
			a[k] = b[i];
			i++;
		}
		else{
			a[k] = b[j];
			j++;
		}
	}
	while(i<=mid) a[k++] = b[i++];
	while(j<=high) a[k++] = b[j++];
}
void MergeSort(ElemType a[], int low, int high){
	if(low<high){
		int mid = (low + high)/2;
		MergeSort(a, low, mid);
		MergeSort(a, mid+1, high);  // 这两步只是进行划分,最后是 mid = low+1, high = low+2
 		Merge(a, low, mid, high);
	}
}

其他非比较排序

1. 计数排序(一个桶指示一个数字,用下标对应的值指示数字的大小, 数组的元素值指示数字的多少)

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

算法描述

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

动图演示

在这里插入图片描述

// malloc, memset
// https://stackoverflow.com/questions/13410550/do-i-have-to-call-memset-after-i-allocated-new-memory-using-malloc
void CountingSort(int a[], int len, int max_value){
	int *b = (int*) malloc((max_value+1)*size_of(int));
	memset(b,0,(max_value+1)*size_of(int));
	
	// 遍历数组a
	for(int i = 0; i<len; i++){
		b[a[i]]++ ;
	}
	
	// 遍历桶
	int k = 0;
	for(int j = 0; j<max_value; j++){
		while(b[j]>0){
			a[k++] = b[j];
			b[j]--;
		}
	}
}

2. 桶排序(一个桶指示一个范围内数字(用hash来映射),用下标对应的值指示范围, 数组的元素是动态长度的数组,放着在同一范围内的数据)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

图片演示

在这里插入图片描述

复杂度分析

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

代码实现

 设置桶的默认数量为5
const int BUCKET_SIZE = 5;
void BucketSort(int a[], int len){
	int min_value = a[0];
	int max_value = a[0];
	for(int i=0; i<len; i++){
		if(a[i] < min_value){ min_value = a[i];}
		else if(a[i] > max_value){ max_value = a[i];}
	}
	int bucket_count = (max_value - min_value) / BUCKET_SIZE + 1;
	
	// 桶的初始化
	vector<vector<int>> buckets;
	while(bucket_count--){
		buckets.push_back(vector<int>());
	}
	
	// 利用映射函数将数据分配到各个桶中
	for(int i=0; i<len; i++){
		bucket[(a[i]-min_value)/BUCKET_SIZE].push_back(a[i]);
	}
	
	int index = 0;
	for(int j=0; j<bucket.size(); j++){
		// 对每个桶进行排序,这里使用了内置sort函数,也可使用插入排序等方法
		sort(bucket[j].begin(), bucket[j].end());  // vector的排序!
		for(int k = 0; k<bucket[j].size(); k++){
			a[index++] = bucket[j][k];
		}
	}
}

3. 基数排序(共有的n个数字位(eg,13:2),每个数字位可能有k种取值)

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

算法描述

取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);

算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

动图演示

在这里插入图片描述

复杂度分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

代码实现

//int counter[];
void radixSort(int a, int len, int max_digit) {
    int mod = 10;
    int dev = 1;
    vector<vector<int>> counter;
    for (int i = 0; i < max_digit; i++, dev *= 10, mod *= 10) {
        for(int j = 0; j < len; j++) {
        	// bucket 是对应这个数位上有几个可能的值,0-9一般都有可能
            int bucket = (a[j] % mod) / dev;
            counter[bucket].push_back(a[j]);
        }
		sort(counter[bucket].begin(), counter[bucket].end());	
        int index= 0;
        for(int j = 0; j < counter.size(); j++) {
            for(int k = 0; k<counter[j].size(); k++){
				a[index++] = counter[j][k];
            }
        }
    }
    return arr;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值