常用排序算法
一、排序的概述
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->
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-> |
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) | 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 |
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的位置
分别进入左边和右边进行相同的过程,可以用函数递归
升序
12 | 56 | 78 | 10 | 08 | 提取出12 |
---|---|---|---|---|---|
i-> | <-j | ||||
08 | 56 | 78 | 10 | × | i 所指数值换成 j 所指比12小的值,然后向右走 |
i | j | ||||
08 | × | 78 | 10 | 56 | j 所指数值换成 i 所指比12大的值,然后向左走 |
->i | j | ||||
08 | 10 | 78 | × | 56 | |
i | j<- | ||||
08 | 10 | × | 78 | 56 | |
->i | j | ||||
08 | 10 | 12 | 78 | 56 | 第一轮结束 |
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
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 |
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) | 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) |
指针写法:
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
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 |
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}
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 | |
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函数的方法很多,原则是尽可能将记录均匀分布,以减少冲突现象的发生。以下是几种常用的构造方法:
- 直接地址法
- 数字分析法
- 平方取中法
- 保留除数法
- 随机函数法
- 叠加法