数据结构 15种内部排序方法 --- c语言

这篇博客详细介绍了15种内部排序方法,包括直接插入排序、折半插入、希尔排序、二叉树排序、选择排序、堆排序、冒泡排序、鸡尾酒排序、归并排序、快速排序、基数排序、计数排序等。每种排序算法都有简述,重点讨论了它们的原理和适用场景,特别适合C语言编程者参考。

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

内部排序

在这里插入图片描述

排序是计算机内经常进行的一种操作,其目的是将一组"无序"的记录序列调整为"有序"的记录序列。分内部排序和外部排序。若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。

将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

排序方法合集
插入:直接插入 折半插入 希尔插入 二路插入
选择:直接选择 二路选择 堆排序 二叉树
交换: 冒泡(起冒) 鸡尾酒 快速排序
归并:二路归并
基数排序
计数排序

直接插入排序

1.简单方法
首先在当前有序区R[1…i-1]中查找R[i]的正确插入位置k(1≤k≤i-1);然后将R[k…i-1]中的记录均后移一个位置,腾出k位置上的空间插入R[i]。
注意:若R[i]的关键字大于等于R[1…i-1]中所有记录的关键字,则R[i]就是插入原位置。

2.改进的方法
一种查找比较操作和记录移动操作交替地进行的方法。具体做法:
将待插入记录R[i]的关键字从右向左依次与有序区中记录Rj的关键字进行比较:
① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;
②若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。
关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。

void insertSort(int arr[],int n){
	int i,j;
	//   i=0,只有一个元素  本身就是有序,不需要插入
	//i=1 从第二个元素开始,逐一往前插入  插入之后使之保持有序 
	for(i=1;i<n;i++){//i=1  1+2+3+4+...+n-1    n(n-1)/2
		int key = arr[i];//提前保存要插入的元素 因为前面的元素往后移,会覆盖
		//[0,i-1]有序  插入key之后使   [0,i]区间有序
		for(j=i-1;j>=0&&arr[j]>key;--j){
			arr[j+1] = arr[j];
		} 
		if(i!=j+1){//i==j+1  
			arr[j+1] = key;   //arr[i] = key; 
		}
	} 
}
/*
下标为i的元素要插入到[0,i-1]区间,使插入arr[i]元素
	局部 [0,i]区间保持有序
	把前面的数据作为有序序列,逐次插入一个元素之后,使这保持有序
	在最开始时,只有一个元素的时候,有序的
*/

折半插入

方法:

在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。

void insertNum(int arr[],int len,int key){
	int left,mid,right;
	for(left=0,right=len-2;left<=right;){//left>right
		mid = (left+right)/2;
		if(key<arr[mid]){
			right = mid-1;
		}else{
			left = mid+1;
		}
	} 
	int i;
	for(i=len-2;i>right;--i){
		arr[i+1] = arr[i];
	}
	arr[right+1] = key;
	
}
void showArr(int arr[],int len){
	int i;
	for(i=0;i<len;i++){
		printf("%d ",arr[i]);
	}
	printf("\n");
}
/*
效率最低的情况:
原始数列: 10 9 8 7 6 5 4 3 2 1   升序
	原始序列和最终的序列正好相反的情况,每次插入一个元素
	都需要把该元素插入到最前面,需要把该元素之前所有的元素都往后移一次
*/

2-路插入排序

2-路插入排序算法是在折半插入排序的基础上对其进行改进,减少其在排序过程中移动记录的次数从而提高效率。利用一个与排序序列一样大小的数组作为辅助空间,设置left和right指针标记辅助数组开始位置和最后位置。遍历未排序序列,如果待插入元素比已排序序列最小的元素(left位置)小,则left位置前移,将待排序元素插入left位置;如果待插入元素比已排序序列最大的元素(right位置)大,则right位置后移,将待排序元素插入right位置;如果待插入元素比最小大,比最大小,则需要移动元素,过程类似直接插入排序,不过考虑到循环使用数组,对于下标的处理有些许不同。

void twoRoadInsert(int arr[],int n){
	int *brr = (int *)malloc(sizeof(arr[0])*n);//brr
	int left = -1,right = n;
	int i,j;
	for(i=0;i<n;i++){
		if(left==-1||arr[i]>brr[0]){//大于最左端的元素 
			for(j=left;j>=0&&arr[i]<brr[j];--j){
				brr[j+1] = brr[j];
			}
			brr[j+1] = arr[i];
			++left;
		}else{//小于brr[0] 如果要插入左边 左边所有的元素都要移动 
			for(j=right;j<n&&arr[i]>brr[j];++j){
				brr[j-1] = brr[j];
			}
			brr[j-1] = arr[i];
			--right;
		} 
	}
	for(i=right;i<n;i++){
		arr[i-right] = brr[i];
	}
	for(i=0;i<=left;i++){
		arr[n-right+i] = brr[i];
	}
	free(brr);
}

希尔排序

//时间复杂度是O(nlogn)    希尔排序  O(n^1.3)  空间复杂度O(1)  不稳定 
void shellSort(int arr[],int n){
	//分组  步长    分为step小组   组内元素步长step 
	int step,i,j;
	for(step = n/2;step>0;step=step/2){
		//组内排序  [0,step) 组内的第一个元素 
		for(i=step;i<n;i++){//除了组内第一个元素 其它元素都需要进行组内插入 
			int key = arr[i];
			for(j=i-step;j>=0&&key<arr[j];j=j-step){
				arr[j+step] = arr[j];//组内的元素往移 
			} 
			arr[j+step] = key; 
		} 
	} 	
}

二叉树排序

ps:具体思想看之前的二叉树算法

void binInsertSort(int arr[],int n){
	int left,right,mid,i,j,key;
	for(i=1;i<n;i++){//arr[1]----arr[n-1]所有的元素依次往前插入 
		key = arr[i];//保存要插入的元素 arr[i]   arr[0]--arr[i-1] 
		for(left=0,right=i-1;left<=right;){//找到arr[i]插入的位置  折半查找 
			mid = (left+right)/2;
			if(key < arr[mid]){
				right = mid-1;
			}else{
				left = mid+1;
			}
		}
		//right+1 位置要存储arr[i] key 
		for(j=i-1;j>right;--j){
			arr[j+1] = arr[j];
		}
		arr[right+1] = key;
	}
}

选择排序

在这里插入图片描述

定义

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。

算法

对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面"后一个元素"现变成了"前一个元素",继续跟他的"后一个元素"进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。

void choiceSort(int arr[],int n){
	int i,j,max;
	for(i=0;i<n-1;i++){//进行n-1次循环  每次选择一个最大值 放到指定位置
		//在指定区间选择最大值
		max = 0;
		for(j=1;j<n-i;j++){//n-1-i  [0,n-1]  [0,n-2]
			if(arr[max]<arr[j]){//1 8 9 7 9 2 3
				max = j;
			}
		}	
		if(max != n-1-i){
			int tmp = arr[max];
			arr[max] = arr[n-1-i];
			arr[n-1-i] = tmp;
		}
	}
} 

void twoChoiceSort(int arr[],int n){
	int i,j,max,min,tmp;
	for(i=0;i<n/2;i++){\
		max = min = i;
		for(j=i+1;j<n-i;j++){//[i,n-1-i]
			if(arr[max] < arr[j]){
				max = j;
			}else if(arr[min] > arr[j]){
				min = j;
			}
		}
		if(max != n-1-i){
			tmp = arr[max];
			arr[max] = arr[n-1-i];
			arr[n-1-i] = tmp;
		}
		if(min == n-1-i){
			min = max;
		}
		if(i != min){
			tmp = arr[i];
			arr[i] = arr[min];
			arr[min] = tmp;
		}
	}
}
/*
二叉树可以用数组来存储(不需要用指针指向左右子树)
	对于下标为i的结点,左子树下标为2*i+1,右子树下标为2*i+2
	对于完全二叉树而言,完全可以用数组来存储表示
	但对于非完全二叉树,内存的利用率不太高

堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

//index要调整的结点  n是总共结点数 
void reHeap(int arr[],int index,int n){
	int child = 2*index+1;
	int key = arr[index];//需要调整的结点的元素 
	while(child<n){//叶子结点存在 
		if(child+1<n && arr[child] < arr[child+1]){//右孩子存在且比左孩子大 
			++child;	
		} 
		//child记录孩子中更大的那个结点下标
		if(key<arr[child]){
			arr[index] = arr[child];//把孩子的元素放在父结点处 
			index = child;
			child = 2*index+1;
		}else{
			break;
		}
	} 
	arr[index] = key;
} 
//时间复杂度是O(nlogn)  空间复杂度O(1)  不稳定 
void heapSort(int arr[],int n){
	//把完全二叉树调整成大根堆
	int i;
	for(i=n/2;i>=0;--i)//第一个非叶子结点 n/2   从下往上调整 
		reHeap(arr,i,n); 
	for(i=0;i<n-1;i++){//循环进行n-1次
		//arr[0]和arr[n-1-i]最后一个元素交换 
		int tmp = arr[0];
		arr[0] = arr[n-1-i];
		arr[n-1-i] = tmp;
		//不考虑最后一个元素的基础上,重新调整成大根堆  只需要调整0结点 
		reHeap(arr,0,n-1-i);
	}
}

冒泡排序

在这里插入图片描述

算法原理

冒泡排序算法的运作如下:(从后往前)

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

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

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

4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

void bubbleSort(int arr[],int n){
	int i,j,tmp;
	for(i=0;i<n-1;i++){
		int swap = false;
		for(j=1;j<n-i;j++){
			if(arr[j] < arr[j-1]){
				tmp = arr[j];
				arr[j] = arr[j-1];
				arr[j-1] = tmp;	
				swap = true;
			}
		}
		if(!swap){//在一次完整的冒泡过程中,没有任何两个元素交换位置 说明已经有序 
			break;
		}
	}
}

鸡尾酒排序

就是冒泡排序的变形,不过时间复杂度,空间复杂度没有变,有些鸡肋

//时间复杂度O(n^2)  空间复杂度O(1)  稳定 
void cookTailSort(int arr[],int n){
	int i,j,tmp;
	for(i=0;i<n/2;i++){
		bool swap = false;
		for(j=i+1;j<n-i;j++){
			if(arr[j]<arr[j-1]){
				tmp = arr[j];
				arr[j] = arr[j-1];
				arr[j-1] = tmp;
				swap = true;	
			}
		}
		for(j=j-2;j>i;j--){
			if(arr[j]<arr[j-1]){
				tmp = arr[j];
				arr[j] = arr[j-1];
				arr[j-1] = tmp;
				swap = true;
			}
		}
		if(!swap){
			break;
		}
	}
} 

归并排序

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

算法描述

归并操作的工作原理如下:

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

第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置

第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾

以下给出两种归并的方法

void megerArr(int arr[],int n){
	int mid = n/2;  //[0,mid)  [mid,n-1]有序	
	int brr[mid];  //brr 保存前半部分数据  
	int i,j,k;
	for(i=0;i<mid;i++)
		brr[i] = arr[i];
	i=0;j=mid;k=0;
	while(i<mid&&j<n){//i brr[0]-brr[mid-1]  j arr[mid]--arr[n-1]   
		if(brr[i]<arr[j]){
			arr[k++] = brr[i++];
		}else{
			arr[k++] = arr[j++];
		}
	}
	while(i<mid){
		arr[k++] = brr[i++];
	}
	//如果是j<n 没有必要再赋值一次 while(j<n){arr[k++] = arr[j++];} 
} 
//时间复杂度O(nlogn)  空间复杂度O(nlogn) 
void megerSort(int arr[],int n){
	//megerSorts(arr,0,n-1);
	if(n<=1)
		return;
	int mid = n/2; //n=2  n==1  [0,1)  [1,1]   3/2 = 1
	megerSort(arr,n/2);//对前半部分进行排序
	megerSort(arr+n/2,n-n/2);
	megerArr(arr,n);//[0,n/2) 升序 [n/2,n-1]升序 合并成有序 
}
void megerArrs(int arr[],int left,int right){
	int mid = (left+right)/2;
	//[left,mid]  [mid+1,right]
	int llen = mid-left+1;
	int rlen = right-mid;
	int brr[llen];
	int i,j,k;
	for(i=0;i<llen;i++)
		brr[i] = arr[left+i];
	i=0;j=mid+1;k=left;
	while(i<llen && j<=right){
		if(brr[i]<arr[j]){
			arr[k++] = brr[i++];
		}else{
			arr[k++] = arr[j++];
		}
	}
	while(i<llen){
		arr[k++] = brr[i++];
	}
}
void megerSorts(int arr[],int left,int right){//[left,right]
	if(left>=right)
		return;
	int mid = (left+right)/2;//[left,mid]  [mid+1,right]
	if(mid-left>0)//左边大于1个元素 
		megerSorts(arr,left,mid);
	if(right-mid-1>0)//右边大于1个元素 
		megerSorts(arr,mid+1,right);
	megerArrs(arr,left,right); 
}


快速排序

它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

也给出两种快速排序的方法

void quickSort(int arr[],int n){//快速排序 
	if(n<=1) return;
	int key = arr[0];
	int i,j;
	for(i=0,j=n-1;i<j;){	
		while(i<j && arr[j]>=key) --j;//从右边找到一个小于key的值 放到i
		if(i<j) arr[i++] = arr[j]; 
		while(i<j && arr[i]<=key) ++i;//从在边找到一个大于key的值 放到j
		if(i<j) arr[j--] = arr[i]; 
	} 
	arr[i] = key;
	if(i>1)
		quickSort(arr,i);//[0,i-1]  i
	if(n-i-1>1) // [i+1,n-1]  n-1-i
		quickSort(arr+i+1,n-i-1);
}

void quickSorts(int arr[],int left,int right){
	if(left>=right)
		return;
	int key = arr[left];
	int i=left,j=right;
	while(i<j){
		while(i<j && arr[j]>=key) --j;
		if(i<j) arr[i++] = arr[j];
		while(i<j && arr[i]<=key) ++i;
		if(i<j) arr[j--] = arr[i];
	}
	arr[i] = key;
	quickSorts(arr,left,i-1);//if(i-left>1)
	quickSorts(arr,i+1,right);//if(right-i>1)
}

基数排序

基数排序(radix sort)属于"分配式排序"(distribution sort),又称"桶子法"(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些"桶"中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
以LSD为例,假设原来有一串数值如下所示:

73, 22, 93, 43, 55, 14, 28, 65, 39, 81

首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:

0

1 81

2 22

3 73 93 43

4 14

5 55 65

6

7

8 28

9 39

折叠第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:

81, 22, 73, 93, 43, 14, 55, 65, 28, 39

接着再进行一次分配,这次是根据十位数来分配:

0

1 14

2 22 28

3 39

4 43

5 55

6 65

7 73

8 81

9 93

折叠第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:

14, 22, 28, 39, 43, 55, 65, 73, 81, 93

这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个"桶子"中建立"子桶",将每个桶子中的数值按照下一数位的值分配到"子桶"中。在进行完最低位数的分配后再合并回单一的数组中。

//空间复杂度O(r+n)  
void baseSort(int arr[],int n){
	SLinketList  list[10] = {NULL};
	int i,j;
	for(i=0;i<10;i++)
		list[i] = slinket_list_create();
	int max = 0;
	for(i=1;i<n;i++){
		if(arr[max]<arr[i])
			max = i;
	}
	int key = arr[max];//最大值
	int div = 1;
	for(;key/div!=0;div=10*div){
		for(i=0;i<n;i++){
			int w = arr[i]/div%10;
			slinket_list_insert_back(list[w],arr[i]);
		}
		for(i=0,j=0;i<10;i++){
			while(!slinket_list_is_empty(list[i])){
				slinket_list_delete_front(list[i],&arr[j]);
				++j;
			}
		}
	} 
	for(i=0;i<10;i++){
		slinket_list_destroy(list[i]);
	}
}

计数排序

计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n))

算法分析:

计数排序对输入的数据有附加的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

时间复杂度为O(n)的排序,最快的内部排序

//时间复杂度O(max-min+n)   空间复杂度O(max-min+1) 
int countSort(int arr[],int n){
	int min = arr[0],max = arr[0];
	int i,j;
	for(i=0;i<n;i++){
		if(max<arr[i]){
			max = arr[i];
		}else if(min>arr[i]){
			min = arr[i];
		}
	}
	int count = max-min+1;
	int *cuts = calloc(count,sizeof(int));//自动清0
	for(i=0;i<n;i++){
		cuts[arr[i]-min]++;
	} 
	for(i=0,j=0;i<count;i++){
		while(cuts[i]>0){
			arr[j++] = i+min;
			--cuts[i];
		}
	}
	free(cuts);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HOVL_C++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值