C语言_常用排序算法

常用排序算法

一、排序的概述

1.排序的概述:

  • 排序是将无序的记录序列调整成有序序列。

  • 对记录进行排序有重要意义。如果记录按key有序,可对其折半查找,提高查找效率。

  • 在数据库和文件库等系统中,一般要建立若干索引文件(记录),就牵涉到排序问题。

  • 设含有n个记录(record)的文件f={R1 R2 … Rn},相应记录关键字(key) 集合k={k1 k2 … kn}。若对1,2,…,n的一种排列:
    P(1)P(2)…P(n) (1<=P(i)<=n,i!=j时,P(i)!=P(j))

    Kp(1)<Kp(2)<…Kp(n)
    Kp(1)>Kp(2)>…Kp(n)

2.排序的分类:
稳定排序和非稳定排序

  • 例:文件记录中有两个相同的关键字,若排序后两个关键字所对应的信息的次序没有改变则为稳定排序,若产生改变比如次序颠倒则为非稳定排列。后面所述基本都为非稳定排序。

内排序和外排序

  • 内排序:若待排文件f在计算机的内存储器中,且排序过程也在内存中进行,则称这种排序为内排序。

    • 优点:排序速度快
    • 缺点:长度(记录个数)受限
  • 外排序:若排序中的文件存入外存储器,排序过程借助于内外存数据交换(或归并)来完成,则称其为外排序。

  • 后面重点讨论都为内排序的方法,算法以及时间复杂度分析。

  • 截至目前,各种内排序方法可归纳为以下几类:

    • 交换法排序: 冒泡排序、 快速排序
    • 插入法排序: 直插排序、 希尔排序
    • 选择法排序
    • 归并法排序

排序的对象

  • 顺序结构:类似线性表的顺序存储,将文件存于一片连续的存储空间,逻辑上相邻的记录在存储器中的物理位置也是相邻的

    • 在这种结构上对文件排序,一般要作记录的移动。当发生成片记录移动时,是一个很耗时的工作。
  • 链表结构:类似线性表的链式存储,文件中记录用节点来表示,其物理位置任意,节点之间用指针相连。

    • 链表结构的优点在于排序时无须移动记录,只需修改相应记录的指针即可。

二、交换法——冒泡排序

1.思路:每一轮将相邻位置数比较后交换,总共有长度n-1轮。
2.图解:
12 56 78 10 08 将 i 和 j(即i+1) 位置的数值比较并交换
i-> j-> i和j遍历数组
12 56 10 08 78 第一轮,最大值78到了末尾
i-> j->
12 10 08 56 78
i-> j->
10 08 12 56 78
i-> j->
08 10 12 56 78
i-> j->

1256781008将 i 和 j(即i+1) 位置的数值比较并交换
i->j->i和j遍历数组
1256100878第一轮,最大值78到了末尾
i->j->
1210085678
i->j->
1008125678
i->j->
0810125678
i->j->

3.核心代码:

void bubble(int a[],int len){
	int count=0, *p=NULL;
	for(count=0;count<len-1;count++){
		for(p=a;p<a+len-1-count;p++){
			if(*p>*(p+1))swap(p,p+1);    
		}
	}
}

4.复杂度分析:

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

5.稳定性分析:

  • 稳定排序

6.注意点:

二、交换法——选择排序

1.思想:每一轮指出当前数后面最大或最小的数,并进行交换,总共有长度n-1轮。 时间复杂度O(n^2)

2.图解:示例为升序

(12) 56 78 10 08 i 与后面的各项 j 进行逐个比较,找出最小的值并交换
i j-> m
08 (56) 78 10 12
i j-> m
08 10 (78) 56 12
i j-> m
08 10 12 (56) 78
i m j
08 10 12 56 (78)
i m

(12)56781008i 与后面的各项 j 进行逐个比较,找出最小的值并交换
ij->m
08(56)781012
ij->m
0810(78)5612
ij->m
081012(56)78
i mj
08101256(78)
i m

3.核心代码:

void select_sort(int a[],int len){
	int i,j,pos;
	for(i=0;i<len;i++){
		pos=i;
		for(j=i+1;j<len;j++){
			if(a[pos]>a[j])pos=j;
		}
		if(pos!=i)swap(&a[pos],&a[i]);	
	}
}

4.复杂度分析:

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

5.稳定性分析:

  • 稳定排序

6.注意点:

三、交换法——快速排序

1.思路:随机选一个值为监视值,将小于监视值的放
左边,大于监视值的放右边。

2.图解

  • 双边循环:
    12 56 78 10 08 提取出12
    i-> <-j
    08 56 78 10 ×
    i j
    08 × 78 10 56 i 所指数值换成 j 所指比12小的值,然后向右走
    ->i j
    08 10 78 × 56 j 所指数值换成 i 所指比12大的值,然后向左走
    i j<-
    08 10 × 78 56
    ->i j
    08 10 12 78 56 第一轮结束
    i j<- 最后 i,j 重合的位置就是放入12的位置
    分别进入左边和右边进行相同的过程,可以用函数递归
    升序
1256781008提取出12
i-><-j
08567810×i 所指数值换成 j 所指比12小的值,然后向右走
ij
08×781056j 所指数值换成 i 所指比12大的值,然后向左走
->ij
081078×56
ij<-
0810×7856
->ij
0810127856第一轮结束
i j<-
  • 单边循环:
    12 56 78 10 08 pivot=12, 指针 i 向右走
    piv i->
    12 56 78 10 08 mark记录小于pivot的数列
    m ->i
    12 56 78 10 08 i 向右走,若遇到比pivot小的值则停下
    m ->i
    12 10 78 56 08 m加一,m 与 i 交换值
    ->m i
    12 10 78 56 08 然后 i 继续向右走
    m ->i
    12 10 08 56 78
    ->m i
    08 10 12 56 78 最后将pivot与mark的值交换,pivot左边即为小于他的数,右边都是比他大的数
    m
1256781008pivot=12, 指针 i 向右走
pivi->
1256781008mark记录小于pivot的数列
m->i
1256781008i 向右走,若遇到比pivot小的值则停下
m->i
1210785608m加一,m 与 i 交换值
->mi
1210785608然后 i 继续向右走
m->i
1210085678
->mi
0810125678最后将pivot与mark的值交换,pivot左边即为小于他的数,右边都是比他大的数
m

3.核心代码:
双边循环写法:

  • 数组写法:
void quick_sort1(int arr[],int left, int right){

	int start=left, end=right;
	int key=arr[start];
	
	if(start>=end) return;
	
	while(start<end){				
		while(start<end && key<=arr[end]) end--;			//一定是<=不能< 如果后面值大于或等于标志值则略过
		arr[start]=arr[end];
		while(start<end && key>=arr[start]) start++; 	//一定是>=不能>
		arr[end]=arr[start];
	}
	arr[start]=key;
	
	quick_sort1(arr,left,start-1);
	quick_sort1(arr,start+1,right);
}
  • 指针写法:
void quick_sort(int *start, int *endindex){

	if(start==NULL ||endindex==NULL ||start>=endindex) return;
	int *left =NULL, *right=NULL, key=0;
	left =start, right=endindex, key=*start;
	
	while(left<right){
		while(left<right && key<=*right) right--;
		*left=*right;
		while(left<right && key>=*left) left++;
		*right=*left;
	}
	*left=key;
	
	quick_sort(start, left-1);
	quick_sort(right+1, endindex);
}

扩展 单边循环写法:

void quick_sort2(int* p, int start, int endindex){
	
	if(start>=endindex) return;
	
	int pivot_index=partition(p,start,endindex);
	quick_sort2(p, start, pivot_index-1);
	quick_sort2(p,pivot_index+1,endindex);
	
}

int partition(int* p,int start, int endindex){

	int pivot=*(p+start), mark=start;

	for(int i=start+1;i<=endindex;++i){  
		if(*(p+i)<pivot){
			mark++;
			swap(p+i, p+mark);
		}
	}
	swap(p+start, p+mark);
	return mark;
}
void swap(int *p1,int *p2){
	int temp=*p1;*p1=*p2;*p2=temp;
	//(*p1)^=(*p2);(*p2)^=(*p1);(*p1)^=(*p2);  //使用指针时这种交换方法会出现bug!!
}
	

4.复杂度分析:

  • 时间复杂度:O(NlogN)
  • 空间复杂度:

5.稳定性分析:

  • 稳定排序

6.注意点:

  • 越无序越快,不稳定
  • 当数据量比较大,趋于正态分布时,快速排序更快,牺牲空间换时间, 时间复杂度O(nlogn)~O(n^2)。

四、插入法——直插排序

1.思路:将前面项作为有序序列,将后一项插入前面的有序序列中

(12) 56 78 10 08 默认第一数个为有序序列
j i
(12 56) 78 10 08 i 进入前面的有序序列进行逐个比较,
<-j i
(12 56 78) 10 08
<-j i
(10 12 56 78) 08 若 i 没有找到比他小的值 j,则将j的值往后移,直到遇到小于它的数则停下
<-j i
(08 10 12 56 78)

(12)56781008默认第一数个为有序序列
ji
(1256)781008i 进入前面的有序序列进行逐个比较,
<-ji
(125678)1008
<-ji
(10125678)08若 i 没有找到比他小的值 j,则将j的值往后移,直到遇到小于它的数则停下
<-ji
(0810125678)

指针写法:

void insert_sort(int *p, int len){
	int *pstart=NULL, 													//指向当前要插入的数据
	*pend=NULL, temp=0;
	for(pstart=p+1; pstart<p+len; pstart++){
	
		for(pend=pstart, temp=*pstart;  						//temp将当前需要插入位置的数据拿出来,为插入数
					pend>p && temp<*(pend-1);					//将插入数与已经排序好的序列进行逐个比较
					 pend--){													//从序列末尾向前比较
			*pend=*(pend-1); 											//若插入数没有找到比他小的值,则将值往后移
		}	
		*pend= temp; 													//插入数找到第一个比他小的值,则在当前位置放入插入数
	}
}

数组写法,解释参照上述

void insert_sort(int *p, int len){
	for(int i=1;i<len;++i){
		int val=arr[i], j=i-1;
		for(;j>=0;--j){
			if(val<arr[j]) arr[j+1]=arr[j];
			else break;
		}
		arr[j+1]=val;
	}
}

4.复杂度分析:

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

5.稳定性分析:

  • 稳定排序

6.注意点:

  • 越有序越快
  • 参考斗地主理牌时的插入排序法。

希尔排序

1.思路:优化的插入排序。通过将原数组不断折半排序,将原数组变得更有序,再利用插入排序,时间会比插入更快。

原理:
先将数组拆分,分别排序,将数组显得更加有序,然后再整体排序。会比原来的排序更快。O(n^1.5)
100–>交替分成50份–>25–>12–>6–>3–>1

2.图解:

3 6 4 2 5 1
a b c a b c 将原数组分为a,b,c三组,分别对各组进行插入排序

2 5 1 3 6 4
e f e f e f 第一次排序后再将数组分为e,f两组,对各组进行插入排序

1 3 2 4 6 5
g g g g g g 最后一次折半后增量为1,即对数组直接进行插入排序

1 2 3 4 5 6

364251-
abcabc将原数组分为a,b,c三组,分别对各组进行插入排序
251364
efefef第一次排序后再将数组分为e,f两组,对各组进行插入排序
132465
gggggg最后一次折半后增量为1,即对数组直接进行插入排序
123456

3.核心代码:

void shell_sort(int* arr, int len)
{
	int sep = len / 2;

	while(sep > 0){
		for(int i = sep; i < len; i += 1){
			int val = arr[i], j = i - sep;
			for(; j >= 0; j -= sep){
				if(val < arr[j]) arr[j + sep] = arr[j];
				else break;
			}

			arr[j + sep] = val;
		}

		sep /= 2;
	}
}

4.复杂度分析:

  • 时间复杂度: O(n^1.5)
  • 空间复杂度:

5.稳定性分析:

  • 稳定排序

6.注意点:

归并排序

1.思路:将数组不断分为前后两组有序序列,然后逐个比较数组相应位置的值大小,放入另一个数组中,最后返回给原数组。

2.图解:

1 3 5 | 2 4 6
i j ->k:{1}
1 3 5 | 2 4 6
i j ->k:{1,2}
1 3 5 | 2 4 6
i j ->k:{1,2,3}
1 3 5 | 2 4 6
i j ->k:{1,2,3,4}
1 3 5 | 2 4 6
i j ->k:{1,2,3,4,5}
1 3 5 | 2 4 6
i j ->k:{1,2,3,4,5,6}

135246逐个比较数组相应位置的值大小,放入另一个数组中
ij->k:{1}
135246
ij->k:{1,2}
135246
ij->k:{1,2,3}
135246
ij->k:{1,2,3,4}
135246
ij->k:{1,2,3,4,5}
135246
j->k:{1,2,3,4,5,6}

3.核心代码

  • 图解部分代码

#include <stdio.h>
#define len 9
int main(int argc, char **argv)
{
		//int a[len]={1,3,5,7,9,
							//2,4,6,8};
		int a[len]={1,2,3,4
							,2,4,6,7,8};
	int k[len]={0};

	int sep=len/2;

	int i=0, j=sep,kn=0;

	while(i<sep && j<len)
		{
			if(a[i]>a[j]) 		k[kn++]=a[j++]; 
			else 				k[kn++]=a[i++];
		}
	if(i==sep) 			while(kn<len)k[kn++]=a[j++]; 
	else if(j==len) 	while(kn<len)k[kn++]=a[i++]; 


	for(i=0;i<len;++i)printf("%d ",k[i]);

	return 0;
}

  • 整体代码

void merge_sort_private(int* arr, int left, int right)
{
	if(left >= right) return;

	//1. 拆分
	int l = left, r = right;
	int m = (l + r) / 2;

	//2. 排序
	merge_sort_private(arr, l, m);
	merge_sort_private(arr, m + 1, r);

	//3. 归并
	int i = l, j = m + 1, temp[right - left + 1], idx = 0;
	while(i <= m && j <= right){
		if(arr[i] <= arr[j]){
			temp[idx++] = arr[i++];
		}else{
			temp[idx++] = arr[j++];
		}
	}

	for(; i <= m; ++i) temp[idx++] = arr[i];
	for(; j <= right; ++j) temp[idx++] = arr[j];

	for(i = 0; i < idx; ++i) arr[left + i] = temp[i];
}

void merge_sort(int* arr, int len)
{
	merge_sort_private(arr, 0, len - 1);
}

4.复杂度分析:

  • 时间复杂度:O(NlogN)
  • 空间复杂度:

5.稳定性分析:

  • 稳定排序

6.注意点:

五、查找

  • 顺序查找
  • 二分法查找

六、哈希表 hash

1.hash的基本概念

  • Hash表,又称散列表,杂散表。在前面讨论的顺序,折半,分块查找和树表的查找中,其ASL的量级在O(n)~O(log2n)之间。不论ASL在哪个量级,都与记录长度有关,随着n的扩大,算法效率越来越低。ASL与n有关是因为记录在存储器中的存放是随机的,或者说记录的key与记录的存放地址无关,因而查找只能建立在key的"比较"基础上。

  • 理想的查找方法是:对给定的k,不经任何比较便能获取所需要的记录,其查找时间复杂度为常数级O©如O(1)。这就要求在建立记录表的时候,确定记录的key与其存储地址之间的关系f,即 使key与记录的存放地址H相对应。
    key -->f–>H:记录

  • 或称,记录按key存放

  • 之后,当要查找key=k的记录时,通过关系f就可得到相应记录的地址而获取记录,免去了key的比较过程。这个关系f就是所谓的Hash函数(或称散列函数、杂凑函数),记为H(key)。它实际上是一个地址映象函数,其自变量为记录的key,函数值为记录的存储地址(或称Hash地址)。

  • 另外,不同的key可能得到同一个Hash地址,即当key1 != key2时,可能有H(key1)=H(key2),此时称key1和key2为同义词。这种现象称为"冲突"或"碰撞",因为一个数据单位只可存放一条记录。

  • 一般,选取Hash函数只能做到使冲突尽可能少,却不能完全避免。这就要求在出现冲突后,寻求适当的方法(寻找好的hash函数)来解决冲突记录的存放问题。

  • 根据选取的Hash函数H(key)和处理冲突的方法,将一组记录(R1 R2 … Rn)映象到记录的存储空间,所得到的记录表称为Hash表

  • 关于Hash表的讨论关键是两个问题,一是选取Hash函数的方法;二是确定解决冲突的方法。

2.Hash函数的构造方法

  • 选取(或构造)Hash函数的方法很多,原则是尽可能将记录均匀分布,以减少冲突现象的发生。以下是几种常用的构造方法:
    • 直接地址法
    • 数字分析法
    • 平方取中法
    • 保留除数法
    • 随机函数法
    • 叠加法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值