查找
一、查找概论
各位都用过搜索引擎。搜索引擎的大概工作原理就是利用网络“爬虫”抓取并复制网页,并且可以通过该网页的链接来抓取更多的网页。
那么,搜索引擎的是通过什么来抓取网页的呢?就是通过“关键字”来识别网页并抓取网页的。
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。
关键字(Key)是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项(字段)。
若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)。
例如,若将一张成绩单作为查找表,则学号或姓名都可以作为主关键字,因为它们可以唯一确定一个查找记录。而成绩则只能作为次关键字,因为不能唯一确定一个查找记录。
二、顺序表查找
//查找代码
#include <stdio.h>
#include <stdlib.h>
int number[11]={0,12,16,24,35,47,59,62,73,88,99};//待查表,第一个元素是哨兵位而不是待查数据,待查数据从下标1(第二个元素)开始
int Sequential_Search(int *a,int n,int key) //顺序查找
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
return i;//返回查找到的下标
}
return 0;//返回0代表查找失败
}
int Sequential_Search2(int *a,int n,int key) //顺序查找
{
int i;
a[0]=key;//设置哨兵
i=n;
while(a[i]!=key)
{
i--;
}
return i;//返回0则查找失败
}
int Binary_Search(int *a,int n,int key) //有序表查找 - - 二分法查找
{
int low,high,mid;
low=1;
high=n;
while(low<=high)
{
mid=(low+high)/2;
if(key<a[mid])
high=mid-1;
else if(key>a[mid])
low=mid+1;
else
return mid;
}
return 0;
}
int Interpolation_Search(int *a,int n,int key) //插值查找
{
int low,high,mid;
low=1;
high=n;
while(low<=high)
{
mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);
if(key<a[mid])
high=mid-1;
else if(key>a[mid])
low=mid+1;
else
return mid;
}
return 0;
}
int main()
{
int position;
//position = Binary_Search(number,10,47);
//printf("Position is %d\n",position);
return 0;
}
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
1、顺序表查找算法
int Sequential_Search(int *a,int n,int key) //顺序查找
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
return i;//返回查找到的下标
}
return 0;//返回0代表查找失败
}
这段代码比较简单。需要注意的是待查表的数据是从下标1开始的。
2、顺序表查找算法优化
上文所给的算法并非完美,因为每次循环时都需要对i是否越界(即i<=n)进行判断。事实上,我们有更优化的算法。设置一个哨兵,可以解决不需要每次让i与n进行比较。
int Sequential_Search2(int *a,int n,int key) //顺序查找
{
int i;
a[0]=key;//设置哨兵
i=n;
while(a[i]!=key)
{
i--;
}
return i;//返回0则查找失败
}
此时代码是从尾部开始查找,由于a[0]等于key,也就是说,一定会找到key值的位置,要么是在某个非0位置(表示成功),要么是0(表示失败)。
这种方式是在查找数据的尽头设置“哨兵”免去了每次比较数据后都需要判断数据是否越界,看似差别与原先不大,但在数据量较大时,效率提高很大,是非常常用的技巧。
三、有序表查找
我们可以做一个游戏:给定一个100以内正整数让你猜,我只说“大了”或“小了”。对于这个游戏,从1开始依次数到100当然可以,但是效率太低。我们可以先猜50,然后针对“大了/小了”再决定下次猜的数字(25或75)。我们把这种每次取中间记录的查找方法叫做折半查找
折半查找(Binary Search),又称为二分查找。它的前提是线性表中的记录必须是关键字有序,而且线性表必须采用顺序存储。
折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录关键字,则在中间记录的左半区进行查找;若给定值大于中间记录的关键字,则在中间记录的右半区进行查找。不断重复上述过程直至查找成功(或失败)为止。
int Binary_Search(int *a,int n,int key) //有序表查找 - - 二分法查找
{
int low,high,mid;
low=1;
high=n;
while(low<=high)
{
mid=(low+high)/2;
if(key<a[mid])
high=mid-1;
else if(key>a[mid])
low=mid+1;
else
return mid;
}
return 0;
}
由于折半查找的前提是需要有序表存储,因此如果某个数据表需要频繁执行插入或删除操作,维护其有序需要不小的工作量,这时不推荐使用折半查找。
四、散列表(哈希表)查找
上文中介绍的查找方法,都需要“比较”。我们发现,为了查找结果,“比较”是不可避免的,但是否真的有必要呢?
1、散列表查找定义
试想这样的场景:你来到某所不熟悉的学校,想要找一个叫“盖伦”的同学。有两种方法:
1)询问教务处的老师,老师会拿出学生名册,从上到下查找,最终告诉你:“盖伦”同学在德玛西亚班
2)去操场打听认识“盖伦”的同学,他会告诉你:“盖伦”同学在德玛西亚班
以上两种方法都可以查到“盖伦”同学的位置,第一种方法就是常规的查找方法,而第二种方法就是散列表查找方法。
也就是说,我们只需要得到某个函数f(),使得:
存储位置=f(关键字);
那样我们就可以不用通过比较关键字来查找存储位置,而是直接通过该函数算出存储位置。这就是另一种存储技术——散列技术。
对应函数f称为散列函数,又称为哈希(Hash)函数。使用散列技术将记录存储在一块存储空间中,这块存储空间就称为散列表或哈希表(Hash Table)。关键字所对应的记录存储位置就称为散列地址。
2、散列表查找步骤
整个散列过程其实只有两步:
1)在存储时,通过散列函数计算记录的散列位置,构造散列表。
2)当查找记录时,通过散列函数计算记录的散列位置。
例如,我们可以构造一种散列函数f,得到下面这张散列表:
编号 姓名 地址
1 盖伦 德玛西亚
2 卡特琳娜 诺克萨斯
3 慎 均衡教派
4 蒙多 祖安
5 提莫 班德尔城
当我们进行查找时,如果查找“慎”这条记录,则我们可以
f(慎)
就会得到“均衡教派”的查找结果。
因此,散列技术既是一种存储方法,也是一种查找方法。
散列技术最适合的问题是查找与给定值相等的记录。对于其他查找方法来说,散列技术不需要比较,而是直接计算其位置,这样就大大提高了效率。
但是,有利就有弊。散列技术的最大问题是冲突。在理想状态下,每一个关键字,通过散列函数计算出来的位置都是不一样的。但现实中我们经常会碰到两个关键字key1!=key2,而计算后f(key1)=f(key2)的情况,这种现象我们称为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)。
3、散列函数的构造方法
由上文我们可以看出,散列技术的最关键技术就是构造一个好的散列函数。构造散列函数需要考虑2条原则:
⒈计算简单
⒉散列地址分布均匀(即尽量减少冲突)
1)直接定址法
直接定址法就是取关键字的某个线性函数值作为散列地址。即
f(key)=a*key+b(a、b为常数)
例如,现在要对0~100岁的人口统计,则我们可以直接使用年龄的数字作为地址。此时f(key)=key。
地址 年龄 人数
00 0 500万
01 1 600万
……
20 20 1500万
……
这样的散列函数的优点是简单、均匀,不会产生冲突。缺点是需要事先知道关键字的分步情况。适合查找表较小且连续的情况。在现实应用中,此方法虽简单但并不常用。
2)平方取中法
这种计算方法也很简单。例如关键字是1234,它的平方是1522756,我们抽取其中间的三位数227作为散列地址。再比如关键字是4321, 它的平方就是18671041,抽取中间的三位就是671(或710)作为散列地址。
平方取中法适合于不知道关键字分布,而位数又不是很大的情况。
3)除留余数法
此方法是最常用的构造散列函数的方法。对于散列表长度为m的散列函数公式为:
f(key)=key mod p(p<=m)
mod是取模(求余数)的意思。实际上不仅可以直接对关键字取模,也可以在平方取中后再取模。
很显然,除留余数法的关键就在于选取合适的p,p如果选的不好,就很容易产生同义词。
例如:现在有以下关键字列表:
12 15 16 21 22 25 29 38 47 56 67 78
我们选取P为12,则我们可以生成散列表如下:
下标 0 1 2 3 4 5 6 7 8 9 10 11
关键字12 25 38 15 16 29 78 67 56 21 22 47
选取p不合理的话,产生同义词的几率就大很多。例如,如果有以下关键字表
12 24 36 48 60 72 84 96 108 120 132 144
如果我们选取p=12的话,那么求得的地址就都是0。而如果选取p=11的话就好很多
下标 1 2 3 4 5 6 7 8 9 10 0 1
关键字12 24 36 48 60 72 84 96 108 120 132 144
此时只有12和144有冲突,相对于p=12来说就好很多。
根据经验来说,若散列表长度为m,通常p为小于或等于表长(尽量接近m)的质数。
4、处理散列冲突的方法
即使设计再好的散列函数,在实际应用中也不可能完全没有冲突。既然冲突不可避免,就要考虑如何处理它。
1)开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列位置,只要散列表足够大,空的散列地址就一定能找到,并将其存入散列表。
开放定址法又称线性探测法。
2)再散列函数法
我们事先准备多个散列函数,如果发生了冲突,就使用另外的散列函数计算,直至消除冲突为止。这种方法能使得关键字不产生冲突,当然相 应地计算量也增加了。
3)链地址法
如果我们对散列表稍加修改,将普通的散列表变成一个存储链表的表,当发生了冲突时,直接将同义词都存在同一个子表中。这样的话就没有 什么冲突问题了,无论有多少个冲突,只要在当前位置给链表再增加节点即可。
链地址法对于可能会造成很多冲突的散列函数来说提供了绝不会出现冲突的保障,但是也带来了查找时需要遍历链表的额外损耗。
4)公共溢出区法
在散列表外,额外建立一块与原散列表一样大小的缓冲区(溢出表)。如果发生了冲突,冲突的关键字就按顺序存入缓冲区。计算散列地址 时,先与基本表进行比对,如果发现查找不成功,则在溢出表内再进行查找。如果有冲突的数据很少,公共溢出区法查找的性能相当高。
常见查找算法时间复杂度对比
排序
一、排序的基本概念与分类
//排序代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
typedef struct
{
int r[MAXSIZE];//存储待排序数据
int length;//记录顺序表的长度
}Sqlist;
void swap(Sqlist *L,int i,int j)//交换数据函数
{
int temp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = temp;
}
void print(Sqlist *L)//打印数据函数
{
int i;
for(i=0;i<L->length;i++)
printf("%d ",L->r[i]);
printf("\n");
}
void BubbleSort(Sqlist *L)//冒泡排序
{
int i,j;
for(i=0;i<L->length-1;i++)
{
for(j=0;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
swap(L,j,j+1);
}
}
}
void BubbleSort2(Sqlist *L)//冒泡排序改进版1:增加标志位
{
int i,j;
int flag = 1;
for(i=0;i<L->length-1 && flag;i++)
{
flag = 0;
for(j=0;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1);
flag = 1;
}
}
}
}
void BubbleSort3(Sqlist *L)//冒泡排序改进版2:双向移动数据(鸡尾酒排序)
{
int i,j;
for(i=0;i<L->length/2;i++)
{
for(j=i;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
swap(L,j,j+1);
}
for(j=L->length-1-(i+1);j>i;j--)
{
if(L->r[j]<L->r[j-1])
swap(L,j-1,j);
}
}
}
void SelectSort(Sqlist *L)//直接选择排序
{
int i,j,min;//min是当次循环的最小值的下标
for(i=0;i<L->length;i++)
{
min=i;
for(j=i+1;j<L->length;j++)
{
if(L->r[min]>L->r[j])
min=j;
}
if(i!=min)
swap(L,i,min);
}
}
void InsertSort(Sqlist *L)//直接插入排序
{
int i,j,tmp;
if(L->r[0]>L->r[1])//首先保证前2个元素有序,这样后续元素才能插入
swap(L,0,1);
//for(i=1;i<L->length;i++)//1个数是序列
for(i=2;i<L->length;i++)//插入L->r[i]元素
{
if(L->r[i]<L->r[i-1])
{
tmp=L->r[i];
for(j=i-1;L->r[j]>tmp&&j>=0;j--)//将所有大于L->r[i]元素都后移,空出位置
L->r[j+1]=L->r[j];
L->r[j+1]=tmp;//插入正确位置
}
}
}
void ShellSort(Sqlist *L)
{
int i,j,tmp;
int increment;
for(increment=1;increment<=L->length/4;increment=increment*2+1);//计算合适增量
//该增量计算方法由Hibbard于1963年发表,增量为1,3,7,15……2^i-1,最接近n/4的值
//使用该种增量可使得时间复杂度达到O(n^(3/2))
//printf("increment是%d\n",increment);
for(;increment>0;increment=(increment-1)/2)
{
for(i=increment;i<L->length;i++)
{
tmp = L->r[i];
for(j=i-increment;j>=0&&tmp<L->r[j];j-=increment)
L->r[j+increment]=L->r[j];
L->r[j+increment]=tmp;
}
}
}
void QSort1(Sqlist *L,int left,int right)//快速排序
{
int i=left,j=right;
if(left>=right)//递归出口
return;
int key = L->r[left];
while(i<j)
{
while(L->r[j]>=key && i<j)
j--;
L->r[i]=L->r[j];
while(L->r[i]<=key && i<j)
i++;
L->r[j]=L->r[i];
}
L->r[i]=key;
QSort1(L,left,i-1);
QSort1(L,i+1,right);
}
/*快速排序算法写法2*/
int Partition(Sqlist *L,int low,int high)
{
int pivotkey,tmp;
pivotkey=L->r[low];
tmp=pivotkey;
while(low<high)
{
while(low<high && L->r[high]>=pivotkey)
high--;
L->r[low]=L->r[high];
while(low<high && L->r[low]<=pivotkey)
low++;
L->r[high]=L->r[low];
}
L->r[low]=tmp;
return low;
}
void QSort2(Sqlist *L,int low,int high)
{
int pivot;
if(low<high)
{
pivot = Partition(L,low,high);
QSort2(L,low,pivot-1);
QSort2(L,pivot+1,high);
}
}
/*快速排序算法写法2end*/
int main()
{
Sqlist data;
data.r[0]=9;data.r[1]=1;data.r[2]=5;data.r[3]=8;data.r[4]=3;data.r[5]=7;data.r[6]=4;data.r[7]=6;data.r[8]=2;data.r[9]=10;
data.length=sizeof(data.r)/sizeof(data.r[0]);
//BubbleSort(&data);
//BubbleSort2(&data);
//BubbleSort3(&data);
//SelectSort(&data);
//InsertSort(&data);
//ShellSort(&data);
//QSort1(&data,0,data.length-1);
//QSort2(&data,0,data.length-1);
print(&data);
return 0;
}
1、排序的定义
假设含有n个记录的序列为{r1,r2,……rn},其相对应的关键字分别为{k1,k2,……kn},需确定一种序列,使其关键字满足k1<=k2<=……<=km(非递减)或k1>=k2>=……>=km(非递增)关系,即使得序列成为一个按关键字有序的序列{r1,r2,……,rm},这样的操作就称为排序。
排序的依据是关键字之间的大小关系,那么,对于同一个记录集合,针对不同的关键字进行排序,可以得到不同的序列。
2、排序的稳定性
假设在排序前,有ki=kj(1<=i<=n,1<=j<=n,i不等于j),且在排序前的序列中ri位置领先于rj(即i<j)。如果排序后ri仍然领先于rj,则称所用的方法是稳定的;反之,若可能使得排序后的序列中rj领先ri,则称所用的排序算法是不稳定的。
例如有序列:
编号 姓名 总分
1 Li 750
2 Liu 730
3 Zhou 738
4 Han 750
此时我们按总分排序,如果得到
1 Li 750
4 Han 750
2 Zhou 738
3 Liu 730
这样排序算法就是稳定的。而如果得到
4 Han 750
1 Li 750
2 Zhou 738
3 Liu 730
则这样的排序算法就是不稳定的。
对于多个关键字排序时,如果有一组关键字会得到不稳定的结果,则我们就认为此排序算法是不稳定的。
3、排序算法的分类
1)按数据位置分类
根据排序过程中待排数据是否全部被放置在内存中,排序分为:内排序和外排序
内排序:排序过程中,待排数据全部被放置在内存中。
外排序:排序过程中,因记录太多,不能同时放在内存中,整个排序过程中需要在内外存之间多次交换数据才能进行。
我们这里只讨论内排序算法。对于内排序来说,排序算法的性能主要受3个方面影响:
⒈时间性能
排序是数据处理时经常执行的操作,往往属于核心代码部分,因此排序算法的时间开销是衡量其好坏的最重要标志。在排序中,主要涉及到两 种操作:比较与移动。高效率的排序算法应该是具有尽可能少的关键字比较次数和尽可能少的数据移动次数。
⒉辅助空间
评价排序算法的另一个主要标准是执行算法所需要的辅助空间。辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的额外存储空间。
⒊算法的复杂性
过于复杂的算法会影响其排序性能。
2)按排序操作分类
根据排序过程中借助的操作,我们把排序分为:插入排序、交换排序、选择排序和归并排序。
3)按算法的复杂性分类
根据排序算法的复杂性分类,可分为简单排序算法和改进排序算法。
简单排序算法:冒泡排序、直接选择排序、直接插入排序
改进排序算法:Shell排序、堆排序、归并排序、快速排序
二、冒泡排序
无论学习哪种编程语言,当学习到循环与数组等概念的时候,通常会介绍一种排序算法来作为例子或练习。而这种排序算法通常都是冒泡排序。
1、最简单的冒泡排序
冒泡排序(Bubble Sort)是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序为止。
在排序过程中,较小的数字(或较大的数字)会如同水下的气泡一样慢慢浮出水面,冒泡排序的命名就此而来。
void BubbleSort(Sqlist *L)//冒泡排序
{
int i,j;
for(i=0;i<L->length-1;i++)
{
for(j=0;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
swap(L,j,j+1);
}
}
}
2、冒泡排序优化1
冒泡排序是否可以进行优化呢?答案是肯定的。
如果待排序数据是基本有序的(例如2,1,3,4,5,6,7,8,9,10,除了第一和第二个关键字不同,需要交换外,其他数据关键字都已经有序。此时我们只需交换这两个数字即可,而无需将冒泡排序执行到底。
我们可以设置一个标志位flag,用它来指示一次冒泡排序执行后是否有数据交换。如果一次排序后没有数据交换,我们就可以认为数据已经有序,无需再继续执行后面的工作了。
void BubbleSort2(Sqlist *L)//冒泡排序改进版1:增加标志位
{
int i,j;
int flag = 1;
for(i=0;i<L->length-1 && flag;i++)
{
flag = 0;
for(j=0;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1);
flag = 1;
}
}
}
}
代码改动的关键就是在外层循环for()的结束条件中,增加了对flag是否是true的判断。这样的改进能避免数据在有序的情况下做无意义的循环判断,从而提升效率。
3、冒泡排序优化2
从另一个角度来想,一次循环数据从前扫描到后,然后再从前扫描到后……也就是说,“磁头”扫描一个来回移动一个关键字使其有序。如果我们能在“磁头”移动回表头时,也能移动一个关键字,那么就相当于一次扫描一个来回移动两个关键字,可以提升其执行效率。
void BubbleSort3(Sqlist *L)//冒泡排序改进版2:双向移动数据(鸡尾酒排序)
{
int i,j;
for(i=0;i<L->length/2;i++)
{
for(j=i;j<L->length-i-1;j++)
{
if(L->r[j]>L->r[j+1])
swap(L,j,j+1);
}
for(j=L->length-1-(i+1);j>i;j--)
{
if(L->r[j]<L->r[j-1])
swap(L,j-1,j);
}
}
}
三、直接选择排序
冒泡排序是基于比较和交换的排序,其算法思想就是不断交换,通过交换完成最终排序。而直接选择排序则是基于选择的排序,其算法思想是每次选出待排数据的关键字中最大(或最小)的数据作为第i个记录。
1、直接选择排序算法
选择排序算法(Selection Sort)就是通过n-i次关键字比较,从n-i+1个数据中每次挑选出关键字最小(或最大)的数据并和第i(1<=i<=n)个数据交换之。
void SelectSort(Sqlist *L)//直接选择排序
{
int i,j,min;//min是当次循环的最小值的下标
for(i=0;i<L->length;i++)
{
min=i;
for(j=i+1;j<L->length;j++)
{
if(L->r[min]>L->r[j])
min=j;
}
if(i!=min)
swap(L,i,min);
}
}
注意代码中的min是这次排序过程中最小数据的下标。
从性能上来说,选择排序略优于冒泡排序。
四、直接插入排序
扑克牌是我们都玩过的游戏。那么摸到手的扑克牌如何理牌呢?一般情况下,都是选出一张牌,将它放置在比它大和比它小的两张牌之间。这里我们用于理牌的方法就是直接插入排序。
1、直接插入排序算法
直接插入排序算法(Straight Insertion Sort)的基本操作是将一个数据插入到一个已经排好序的有序表中,从而得到一个新的有序表。重复这个过程,直至所有数据有序。
void InsertSort(Sqlist *L)//直接插入排序
{
int i,j,tmp;
if(L->r[0]>L->r[1])//首先保证前2个元素有序,这样后续元素才能插入
swap(L,0,1);
//for(i=1;i<L->length;i++)//1个数是序列
for(i=2;i<L->length;i++)//插入L->r[i]元素
{
if(L->r[i]<L->r[i-1])
{
tmp=L->r[i];
for(j=i-1;L->r[j]>tmp&&j>=0;j--)//将所有大于L->r[i]元素都后移,空出位置
L->r[j+1]=L->r[j];
L->r[j+1]=tmp;//插入正确位置
}
}
}
void ShellSort(Sqlist *L) //希尔排序
{
int i,j,tmp;
int increment;
for(increment=1;increment<=L->length/4;increment=increment*2+1);//计算合适增量
//该增量计算方法由Hibbard于1963年发表,增量为1,3,7,15……2^i-1,最接近n/4的值
//使用该种增量可使得时间复杂度达到O(n^(3/2))
//printf("increment是%d\n",increment);
for(;increment>0;increment=(increment-1)/2)
{
for(i=increment;i<L->length;i++)
{
tmp = L->r[i];
for(j=i-increment;j>=0&&tmp<L->r[j];j-=increment)
L->r[j+increment]=L->r[j];
L->r[j+increment]=tmp;
}
}
}
需要注意的是,直接插入排序需要一个已经有序的序列作为“基准”。代码中,选区r[0]与r[1]作为基准,在排序前,需要判断r[0]与r[1]的关系保证其是有序表。可以尝试省略掉这一步,观察排序后的内容。
从性能上来说,直接插入排序略优于冒泡排序。
五、快速排序
上文中介绍的的冒泡排序、选择排序、直接插入排序及其改进版本,都属于简单排序算法。因为它们的时间复杂度都为O(n^2)。而改进排序算法(Shell排序、堆排序、归并排序、快速排序)的时间复杂度都为O(nlogn)甚至更快。在这里我们主要学习快速排序。
1、快速排序算法
快速排序算法最早由图灵奖获得者Tony Hoare于1962年设计出来,被称为“20世纪十大算法”之一。
快速排序相当于冒泡排序的升级,二者都属于交换排序类。
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排数据分割成独立的两部分,其中一部分的关键字都比另一部分的关键字小。之后对这两部分分别进行排序,最终达到整体有序。
快速排序算法的文字描述是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j;此时令循环结束。将key值赋值到i(或j)的位置。
6)递归操作数组A[]在key值左的左半部分。
7)递归操作数组A[]在key值右的右半部分。
void QSort1(Sqlist *L,int left,int right)//快速排序
{
int i=left,j=right;
if(left>=right)//递归出口
return;
int key = L->r[left];
while(i<j)
{
while(L->r[j]>=key && i<j)
j--;
L->r[i]=L->r[j];
while(L->r[i]<=key && i<j)
i++;
L->r[j]=L->r[i];
}
L->r[i]=key;
QSort1(L,left,i-1);
QSort1(L,i+1,right);
}
/*快速排序算法写法2*/
int Partition(Sqlist *L,int low,int high)
{
int pivotkey,tmp;
pivotkey=L->r[low];
tmp=pivotkey;
while(low<high)
{
while(low<high && L->r[high]>=pivotkey)
high--;
L->r[low]=L->r[high];
while(low<high && L->r[low]<=pivotkey)
low++;
L->r[high]=L->r[low];
}
L->r[low]=tmp;
return low;
}
void QSort2(Sqlist *L,int low,int high)
{
int pivot;
if(low<high)
{
pivot = Partition(L,low,high);
QSort2(L,low,pivot-1);
QSort2(L,pivot+1,high);
}
}
2、快速排序算法的优缺点
快速排序算法之所以叫“快速”排序,意味着目前阶段没有人找到更优秀于这个算法的排序算法。如果某一天有人找到了更好的排序算法,“快速”就会名不副实,不过,至今为止,Tony Hoare发明的排序算法经过多次优化后,在整体性能上,依然是排序算法中的王者。
不过快速排序算法仍有缺陷,快速排序算法虽然对大数据排序十分擅长,但不擅长数据不多时进行排序。在数据不多时,快速排序与冒泡排序几乎看不出时间上的优势,只有数据足够大时,快速排序才能发挥出它的优势。因此我们在对数据进行排序时,若数据量不太多,可以选择使用三种简单排序算法(冒泡排序、选择排序、直接插入排序);若数据量巨大,我们再选择快速排序。
偏注:归并排序
引自:https://blog.csdn.net/chenhuajie123/article/details/9296359
归并排序的定义
归并排序算法采用的是分治算法,即把两个(或两个以上)有序表合并成一个新的有序表,即把待排序的序列分成若干个子序列,每个子序列都是有序的,然后把有序子序列合并成整体有序序列,这个过程也称为2-路归并.注意:归并排序的一种稳定排序,即相等元素的顺序不会改变.
归并排序的原理
常见的排序主要有两种,一种是先把待排序的序列一次分割,使子序列的长度减小至1,然后在合并,另外一种是把待排序两两分组排序然后在合并,具体过程用图来解释:
(1) 先分割再合并
待排序序列(14,12,15,13,11,16)
(2) 分组合并
待排序序列(25,57,48,37,12,92,86)
归并排序实现的示例代码:
#include<stdio.h>
//将有二个有序子数组a[begin...mid]和a[mid+1...end]合并。
void MergeArray(int a[],int begin,int mid,int end,int temp[])
{
int i=begin,j=mid+1;
int m=mid,n=end;
int k=0;
while(i<=m && j<=n)
{
if(a[i]<=a[j])
temp[k++]=a[i++];
else
temp[k++]=a[j++];
}
while(i<=m)
temp[k++]=a[i++];
while(j<=n)
temp[k++]=a[j++];
//把temp数组中的结果装回a数组
for(i=0;i<k;i++)
a[begin+i]=temp[i];
}
void mergesort(int a[],int begin,int end,int temp[])
{
if(begin<end)
{
int mid = (begin+end)/2;
mergesort(a,begin,mid,temp); //左边有序
mergesort(a,mid+1,end,temp); //右边有序
MergeArray(a,begin,mid,end,temp); //将左右两边有序的数组合并
}
}
int main()
{
int num[10]={2,5,9,3,6,1,0,7,4,8};
int temp[10];
mergesort(num,0,9,temp);
for(int i=0;i<10;i++)
{
printf("%d",num[i]);
}
printf("\n");
}
归并排序的时间复杂度
归并排序的最好、最坏和平均时间复杂度都是O(nlogn),而空间复杂度是O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。因此可以看出,归并排序算法比较占用内存,但却是效率高且稳定的排序算法。