![67c5da6f46648bb1b951fe38e5345bf5.png](https://img-blog.csdnimg.cn/img_convert/67c5da6f46648bb1b951fe38e5345bf5.png)
一、排序
![8f8f85fd8957b4b6299908f60f14bb1e.png](https://img-blog.csdnimg.cn/img_convert/8f8f85fd8957b4b6299908f60f14bb1e.png)
(1)冒泡排序:
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
![23dff312b422c06de36a8d19d8a5d4a7.png](https://img-blog.csdnimg.cn/img_convert/23dff312b422c06de36a8d19d8a5d4a7.png)
void BubbleSort(Sqlist &L)
{
m=L.length-1;flag=1;
while ((m>0)&&(flag==1))
{
flag=0;
for(j=1;j<=m;j++)
if(L.r[j].key>L.r[j+1].key)
{
flag=1;
t=L.r[j];L.r[j]=L.r[j+1];
L.r[j=1]=t;
}
--m;
}
}
(2)希尔排序:
希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序。
算法步骤:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
![60c60b116e989975a80595a9057fd52d.png](https://img-blog.csdnimg.cn/img_convert/60c60b116e989975a80595a9057fd52d.png)
![6e5641aa81b664768659f587d8c13d5c.png](https://img-blog.csdnimg.cn/img_convert/6e5641aa81b664768659f587d8c13d5c.png)
void ShellPass(SeqList R,int d)
{//希尔排序中的一趟排序,d为当前增量
for(i=d+1;i<=n;i++) //将R[d+1..n]分别插入各组当前的有序区
if(R.key<R[i-d].key){
R[0]=R;j=i-d; //R[0]只是暂存单元,不是哨兵
do {//查找R的插入位置
R[j+d];=R[j]; //后移记录
j=j-d; //查找前一记录
}while(j>0&&R[0].key<R[j].key);
R[j+d]=R[0];
}
}
void ShellSort(SeqList R)
{
int increment=n; //增量初值,不妨设n>0
do {
increment=increment/3+1; //求下一增量
ShellPass(R,increment); //一趟增量为increment的Shell插入排序
}while(increment>1)
}
(3)归并排序:
归并排序假设所有数据分成两组,每组都是已经排序好的。每次从两组数据中找出各自最小的并选择更小的那个转移到另外一个数组中,当所有数据都转移完成后排序结束。把新数组中所有数字按顺序转移回原数组。
在进行归并之前先针对两组数字递归调用归并排序函数。
如 设有数列{6,202,100,301,38,8,1}
初始状态:6,202,100,301,38,8,1
第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;
第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;
第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
总的比较次数为:3+4+4=11;
逆序数为14;
![5ee93835818a76dd30a4dcfee4a978fc.png](https://img-blog.csdnimg.cn/img_convert/5ee93835818a76dd30a4dcfee4a978fc.png)
实现一:
void outer_merge(int data1[],size_t size1,int data2[],size_t size2,int data3[]){
//合并有序数组
int i = 0,j = 0,k = 0;
for (;k < size1 + size2;k++){
if (i < size1 && j < size2){//需要选择一个放入(比较)
if (data1[i] < data2[j]){
data3[k] = data1[i++];
}
else
data3[k] = data2[j++];
}
else{//只能从一个数据源放入(不比较值)
if (i < size1)
data3[k] = data1[i++];
else if(j < size2)
data3[k] = data2[j++];
}
}
}
void inner_merge(int data[],size_t left,size_t mid,size_t right){
//数组由2个有序的字数组组成
size_t size = (right - left + 1) * sizeof(data[0]);
int *temp = malloc(size);
outer_merge(data + left,mid - left + 1,data + mid + 1,right - mid,temp);//temp有序
memcpy(data + left,temp,size);
free(temp);
}
void merge_sort(int data[],size_t left,size_t right){
//归并排序(数组开始无序)
if (left < right){
int mid = (left + right) / 2;
merge_sort(data,left,mid);//排序mid之前
merge_sort(data,mid + 1,right);//排序mid之后
inner_merge(data,left,mid,right);
}
}
实现二:
void merge_sort(int *p_value,int *p_tmp,int start,int end){
if (end > start){
int mid = (start + end) / 2 + 1;
merge_sort(p_value,p_temp,start,mid - 1);
merge_sort(p_value,p_temp,mid,end);
int first = start,second = mid,temp = start;
while (first < mid || second <= end){
if (first == mid){
*(p_temp + temp) = *(p_value + second);
second++;
}
else if (second > end){
*(p_temp + temp) = *(p_value + first);
first++;
}
else {
if (*(p_value + first) > *(p_value +
second)){
*(p_temp + temp) = *(p_value + second);
second++;
}
else {
*(p_temp + temp) = *(p_value + first);
first++;
}
}
temp++;
}
int pos = 0;
for (pos = start;pos <= end;pos++){
*(p_value + pos) = *(p_temp + pos);
}
}
}
(4)快速排序:
快速排序是由冒泡排序改进而得到的,快速排序方法中一次交换可消除多个逆序。
(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; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的
时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i,
j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
![9226e7d4080bd9a1759a1adae3e27cdd.png](https://img-blog.csdnimg.cn/img_convert/9226e7d4080bd9a1759a1adae3e27cdd.png)
void sort(int *a, int left, int right)
{
if(left >= right)/*如果左边索引大于或者等于右边的索引就代表已经整理完成一个组了*/
{
return ;
}
int i = left;
int j = right;
int key = a[left];
while(i < j) /*控制在当组内寻找一遍*/
{
while(i < j && key <= a[j])
/*而寻找结束的条件就是,1,找到一个小于或者大于key的数(大于或小于取决于你想升
序还是降序)2,没有符合条件1的,并且i与j的大小没有反转*/
{
j--;/*向前寻找*/
}
a[i] = a[j];
/*找到一个这样的数后就把它赋给前面的被拿走的i的值(如果第一次循环且key是
a[left],那么就是给key)*/
while(i < j && key >= a[i])
/*这是i在当组内向前寻找,同上,不过注意与key的大小关系停止循环和上面相反,
因为排序思想是把数往两边扔,所以左右两边的数大小与key的关系相反*/
{
i++;
}
a[j] = a[i];
}
a[i] = key;/*当在当组内找完一遍以后就把中间数key回归*/
sort(a, left, i - 1);/*最后用同样的方式对分出来的左边的小组进行同上的做法*/
sort(a, i + 1, right);/*用同样的方式对分出来的右边的小组进行同上的做法*/
/*当然最后可能会出现很多分左右,直到每一组的i = j 为止*/
}
(5)选择排序:
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
int main()
{
int i,j,t,a[11]; //定义变量及数组为基本整型
printf("请输入10个数:n");
for(i=1;i<11;i++)
scanf("%d",&a[i]); //从键盘中输入要排序的10个数字
for(i=1;i<=9;i++)
for (j=i+1;j<=10;j++)
if(a[i]>a[j]) //如果前一个数比后一个数大,则利用中间变量t实现两值互换
{
t=a[i];
a[i]=a[j];
a[j]=t;
}
printf("排序后的顺序是:n");
for(i=1;i<=10;i++)
printf("%5d", a[i]); //输出排序后的数组
printf("n");
return 0;
}
(6)插入排序:
1.直接插入排序
直接插入排序是一种最简单的排序方法,其基本操作是将一条记录插入到已排好序的有序表中,从而得到一个新的、记录数量增1的有序表。
void InsertSort(SqList &L)
{//对顺序表L做直接插入排序
for(i=2;i<=L.length;++i)
if(L.r[i].key<L.r[i-1].key) //"<",将r[i]插入到有序子表
{
L.r[0]=L.r[i]; //将待插入的记录暂存到监视哨中
L.r[i]=L.r[i-1]; //r[i-1]后移
for(j=j-2;L.r[0].key<L.r[j].key;--j)
L.r[j+1]=L.r[j]; //记录逐个后移,直到找到插入位置
L.r[j+1]=L.r[0];
}
}
2.折半插入排序
①设待排序的记录存放在数组r[1…n]中,r[1]是一个有序数列。
②循环n-1次,每次使用折半查找法,查找r[i]在已排好序的序列r[1…i-1]中的插入位置,然后将r[i]插入表长为i-1的有序序列r[1…i-1],直到将r[n]插入表长为n-1的有序序列中。
void BInsertSort(SqList &l)
{
for(i=2;i<L.length;++i)
{
L.r[0]=L.r[i];
low=1;high=i-1;
while(low<high)
{
m=(low+high)/2;
if(L.r[0].key<L.r[m].key) high=m-1;
else low=m+1;
}
for(j=i+1;j>high+1;--j) L.r[j+i]=L.r[j];
L.r[high+i]=L.r[0];
}
}
二、查找
(1)线性表查找
1.顺序查找
顺序查找的查找过程为:从表的一端开始,依次将记录的关键字与给定的值进行比较,若某个记录的关键字和给定的值相等,则查找成功;反之,若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。时间复杂度O(n)。
int Search(SSTable ST,KeyType key)
{//在顺序表ST中顺序查找其关键字等于key的数据元素
for(i=ST.length;i>=1;--i)
if(ST.R[i].key==key) return i;
return 0;
}
2.折半查找:
折半查找也称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中的元素按关键字有序排列。查找过程:从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间记录的关键字,则在中大于或小于中间记录的那一半中查找,这样重复操作,直到查找成功,或者在某一步中查找区间为空,则代表查找失败。时间复杂度为O(log2n).
int Search(SSTable ST,KeyType key)
{//在顺序表ST中顺序查找其关键字等于key的数据元素
low=1;high=ST.length;
while(low<=high)
{mid=(low+high)/2;
if(key==ST.R[mid].key) return mid;
else if (key<St.R[mid].key) high=mid-1;
else low=mid+1;
}
return 0;
}
3.分块查找:
分块查找又称索引顺序查找,这是一种性能介于顺序查找和折半查找之间的一种查找方式。在此查找方法中,除表本身以外,尚需建立一个“索引表”。将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,以此类推。
● 先选取各块中的最大关键字构成一个索引表;
● 先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
(2)树表的查找
对于需要经常进行插入,删除和查找运算的表,适宜采用二叉查找树结构,在二叉查找树中无论是插入和删除,都需要在二叉树上进行查找, 查找的效率取决于树的形态,二叉树越均匀,树的层次越小,平均查找深度越小,该树的查找效率就越高。( 对坏的情况下,二叉树和单链表上的顺序查找一样,亦是(n+1)/2;在最好的情况下,二叉树是一棵形态与二分查找的判定树相似的二叉排序树,此时它的平均查找长度大约是log2(n) )
1.二叉排序树查找
算法步骤
①若二叉排序树为空,则查找失败,返回空指针
②若二叉排序树非空,将给定值key与根节点的关键字T->data.key进行比较;
●若key等于T->data.key,则查找成功,返回根节点地址。
●若key小于T->data.key,则递归查找左子树。
●若key大于T->data.key,则递归查找右子树。
BSTree SearchBST(BSTree T,KeyType key)
{//在根指针T所指二叉排序树中递归的查找某关键字等于key的数据元素
//若查找成功,则返回指向该数据元素结点的指针,否则返回空指针。
if((!T)||key==T->data.key) reyurn T; //查找结束
else if(key<T->data.key) return SearchBST(T->lchird,key);
else return SearchBST(T->rchird,key);
}
(3)哈希表查找:
哈希查找也称为散列查找。所谓的哈希其实就是在记录的存储位置和记录的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。哈希技术既是一种存储方法,也是一种查找方法。
六种哈希函数的构造方法:
(1)直接定址法:
函数公式:f(key) = a * key + b(a,b为常数)
这种方法的优点是:简单、均匀,不会产生冲突。但是需要事先知道关键字的分布情况,适合查找表较小并且连续的情况。
(2)数字分析法:
也就是取出关键字中的若干位组成哈希地址。比如我们的11位手机号是“187****1234”,其中前三位是接入号,一般对应不同的电信公司。中间四位表示归属地。最后四位才表示真正的用户号。
如果现在要存储某个部门的员工的手机号,使用手机号码作为关键字,那么很有可能前面7位都是相同的,所以我们选择后面的四位作为哈希地址就不错。
(3)平方取中法:
取关键字平方后的中间几位作为哈希地址。由于一个数的平方的中间几位与这个数的每一位都有关,所以平方取中法产生冲突的机会相对较小。平方取中法所取的位数由表长决定。
如:K=456,K^2=207936,如果哈希表的长度为100,则可以取79(中间两位)作为哈希函数值。
(4)折叠法:
折叠法是将关键字从左到右分割成位数相等的几个部分(最后一部分位数不够可以短),然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址。当关键字位数很多,而且关键字中每一位上数字分布大致均匀时,可以使用折叠法。
如:我们的关键字是9876543210,哈希表表长三位,我们可以分为四组:987 | 654 | 321 | 0,然后将他们叠加求和:987+654+321+0 = 1962,再取后三位就可以得到哈希地址为962.
(5)除留余数法:
选择一个适当的正整数p(p<=表长),用关键字除以p,所得的余数可以作为哈希地址。即:H(key) = key % p(p<=表长),除留余数法的关键是选取适当的p,一般选p为小于或等于哈希表的长度(m)的某个素数。
如:m = 8,p=7.
m = 16,p = 13.
m = 32,p = 31.
(6)随机数法:
函数公式:f(key) = random(key). 这里的random是随机函数,当关键字的长度不等时,采用这种方式比较合适。
总之,哈希函数的规则就是:通过某种转换关系,使关键字适度的分散到指定大小的顺序结构中。越分散,查找的时间复杂度就越小, 空间复杂度就越高。哈希查找明显是一种以空间换时间的算法。
上面提到了如何构造一个哈希函数,那就不得不提如何避免冲突的算法。
(1)开放定址法
当冲突发生时,使用某种方法在哈希表中形成一探查序列。然后沿着该探查序列逐个单位的查找,直到找到一个开放的地址(即该地址单元为空)为止。对于哈希表中形成一探查序列时,可以有3种不同的方法:
1.线性探测法:
将散列看成一个环形表,探测序列是(假设表长为m):
H(k),H(k)+1,H(k)+2.....m-1,0,1......H(k)-1。用线性探测法解决冲突时,求下一个开放地址的公式为:Hi = (H(k)+i) MOD m.
2.二次探测法:
二次探测法的探测序列依次是12,-12,22,-22等等。当发生冲突时,求下一个开放地址的公式为:
H2i-1 = (H(k)+i2) MOD m
H2i = (H(k)-i2) MOD m (1=< i <= (m-1)/2 )
优点:减少了堆集发生的可能性;
缺点:不容易探测到哈希表空间。
3.伪随机探测法:
采用随机探测法解决冲突时,下一个开放地址的公式为:Hi = (H(k)+Ri) MOD m。
其中R1,R2,...,Rm-1是一个随机排列。
(2)再哈希法
当冲突发生时,使用另一个函数计算得到一个新的哈希地址,直到冲突不再发生时为止。Hi = RHi(key) i = 1,2,…,k 。其中RHi均是不同的哈希函数。优点是不易产生聚集,缺点是增加了计算时间。
(3)链地址法
将所有关键字为同义词的结点链接在同一个单链表中。若选定的哈希函数所产生的哈希地址为0~m-1,则可以将哈希表定义成一个由m个链表头指针组成的指针数组。优点是:不产生聚集;由于结点空间是动态申请的,故更适合造表前无法确定表长的情况;从表中删除节点容易。
(4)公共溢出区法
假设哈希函数的值域为[0...m-1],则设向量HashTable[0...m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都被填入溢出表中。
在哈希表上进行查找的过程和建表的过程基本一致。假设给定的值为k,根据建表时设定的哈希函数H,计算出哈希地址H(k),若表中该地址对应的空间未被占用。则查找失败。否则将该地址中的节点与给定值k比较,若相等则查找成功,否则按建表时设定的处理冲突方法找下一个地址,如此反复下去,直到找到某个地址空间未被占用(查找失败)或者关键字比较相等(查找成功)为止。
代码如下:
//
// main.c
// HashSearch
//
// Created by chenyufeng on 16/2/17.
// Copyright © 2016年 chenyufengweb. All rights reserved.
//
#include "stdio.h"
#include "stdlib.h"
#define HASHSIZE 7 // 定义散列表长为数组的长度
#define NULLKEY -32768
typedef int Status;
typedef struct{
int *elem; // 数据元素存储地址,动态分配数组
int count; // 当前数据元素个数
}HashTable;
// 散列表表长,全局变量
int m = 0;
void InitHashTable(HashTable *hashTable);
Status Hash(int key);
void Insert(HashTable *hashTable,int key);
Status Search(HashTable *hashTable,int key);
void DisplayHashTable(HashTable *hashTable);
int main(int argc, const char * argv[]) {
int result;
HashTable hashTable;
int arr[HASHSIZE] = {13,29,27,28,26,30,38};
//初始化哈希表
InitHashTable(&hashTable);
/**
* 向哈希表中插入数据;
也就是把元素使用哈希函数映射到哈希表中;
*/
for (int i = 0;i < HASHSIZE;i++){
Insert(&hashTable,arr[i]);
}
//数据已存到哈希表中,打印观察哈希表,元素的位置和原数组是完全不一样的
DisplayHashTable(&hashTable);
//查找数据
result = Search(&hashTable,30);
if (result == -1){
printf("没有找到!");
}else{
printf("在哈希表中的位置是:%dn",result);
}
return 0;
}
//初始化一个空的哈希表
void InitHashTable(HashTable *hashTable){
m = HASHSIZE;
hashTable->elem = (int *)malloc(m * sizeof(int)); //申请内存
hashTable->count = m;
for(int i = 0;i < m;i++){
hashTable->elem[i] = NULLKEY;
}
}
//哈希函数(除留余数法)
Status Hash(int key){
return key % m;
}
//插入
void Insert(HashTable *hashTable,int key){
/**
* 根据每一个关键字,计算哈希地址hashAddress;
*/
int hashAddress = Hash(key); //求哈希地址
/**
* 发生冲突,表示该位置已经存有数据
*/
while(hashTable->elem[hashAddress] != NULLKEY){
//利用开放定址的线性探测法解决冲突
hashAddress = (hashAddress + 1) % m;
}
//插入值
hashTable->elem[hashAddress] = key;
}
//查找
Status Search(HashTable *hashTable,int key){
//求哈希地址
int hashAddress = Hash(key);
//发生冲突
while(hashTable->elem[hashAddress] != key){
//利用开放定址的线性探测法解决冲突
hashAddress = (hashAddress + 1) % m;
if (hashTable->elem[hashAddress] == NULLKEY || hashAddress == Hash(key)){
return -1;
}
}
//查找成功
return hashAddress;
}
//打印结果
void DisplayHashTable(HashTable *hashTable){
for (int i = 0;i < hashTable->count;i++){
printf("%d ",hashTable->elem[i]);
}
printf("n");
}
在C语言编程中,我们常常会设定一些预定义常量,或者说是函数的结果状态码,如下所示:
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
因为在C语言中是没有BOOL布尔这种数据类型的,所以上面的预定义可以简化编程。我们有时候还会进行如下的预定义:
typedef int Status;
表示Status是函数的返回类型,其值是函数结果状态代码。我在上述代码中也使用了这种预定义。
首先恭喜您,能够认真的阅读到这里,如果对部分理解不太明白,建议先将文章收藏起来,然后对不清楚的知识点进行查阅,然后在进行阅读,相应你会有更深的认知。如果您喜欢这篇文章,就点个赞或者【关注我】吧!!