数据结构
前排宣传邓俊辉老师的课程和课件
数据结构:
存储结构:顺序、链表、散列、索引。
逻辑结构912:线性、半线性、非线性。
逻辑结构408:线性,树状,网络,集合
网络和图的区别在于,网络是路上有权重的图
栈
栈混洗
深度为n的栈的栈混洗其数量是卡特兰数。(2*n)!/((n+1)!*(n)!)
二叉树
树和森林
按照左孩子右兄弟的规则重构成二叉树进行操作。
特殊的树
满树:一个深度为k,且有2^k-1个节点的二叉树。(所有叶子在同一层)
拟满树:除了最后一层外,其他层和满树一一对应,最后一层叶子结点从左到右和满树一一对应。
丰满树:除了最后一层外,其他层和满树一一对应,最后一层叶子结点随意的分布在最后一层上。
完全二叉树:就是拟满树,n个节点深度为k的二叉树,其中1……n和满树一一对应。
树的遍历
树的前序遍历相当于二叉树的前序,树的后续遍历相当于二叉树的中序。
遍历二叉树的时候,针对每一个节点有三个操作,分别是
1,work()
2,遍历左子树
3,遍历右子树
前中后的区别,说的是访问该节点的位置。
其中操作23的先后书序是不能变的,那么,操作1有三个候选位置,即:123(前),213(中),231(后)
这就是前序遍历,中序遍历和后序遍历。(附上代码)
bi_tree.c1 #include<stdio.h> 2 3 typedef struct Node{ 4 int v; 5 int lc, rc; 6 }ND,*NP; 7 8 NP root=NULL; 9 ND e[1000]; 10 11 void visit_pre(int x){ 12 if(x==0)return; 13 printf("%d ",e[x].v); 14 visit_pre(e[x].lc); 15 visit_pre(e[x].rc); 16 } 17 18 void visit_mid(int x){ 19 if(x==0)return; 20 visit_mid(e[x].lc); 21 printf("%d ",e[x].v); 22 visit_mid(e[x].rc); 23 } 24 25 void visit_aft(int x){ 26 if(x==0)return; 27 visit_aft(e[x].lc); 28 visit_aft(e[x].rc); 29 printf("%d ",e[x].v); 30 } 31 32 int main(){ 33 FILE *fp; 34 fp=fopen("bi_tree.in","r"); 35 int n,i; 36 fscanf(stdin,"%d",&n); 37 for( i=1;i<=n;i++){ 38 fscanf(stdin,"%d%d%d",&e[i].v,&e[i].lc,&e[i].rc); 39 } 40 visit_pre(1); 41 printf("\n"); 42 visit_mid(1); 43 printf("\n"); 44 visit_aft(1); 45 printf("\n"); 46 fclose(fp); 47 return 0; 48 }bi_tree.in12 1 5 2 2 12 6 3 10 7 4 0 0 5 3 8 6 0 0 7 0 0 8 9 11 9 0 0 10 0 0 11 0 0 12 4 0
线索二叉树
线索二叉树是由二叉树衍生出来的,普通二叉树结点只能保存孩子信息,如果想知道前驱后继,就比较麻烦(路径信息被浪费),于是衍生出了线索二叉树,可以存放前驱后继(按照某种遍历规则(前中后))。
struct Node{
int data;
int LTag;//==0表示有左孩子,==1表示有前驱
Node *lchild;
int RTag;//==0表示有右孩子,==1表示有后继
Node *rchild;
};//没用过,感觉实用性不强。
二叉排序树(BST)
1 //代码编译通过,没有测试逻辑(涉及太多指针操作,不保证正确性) 2 #define MAXN 50 3 typedef struct Node{ 4 int v;//data 5 int h;//height 6 struct Node *lc,*rc,*parent; 7 }*BSTNodePtr,BSTNode; 8 struct BSTTree{ 9 int size;//树的大小 10 BSTNodePtr head; 11 }bt; 12 int BSTSearch(BSTNodePtr ptr,int x,BSTNodePtr parent,BSTNodePtr *last){ 13 //查找成功则返回父母 14 if(!ptr){ 15 *last=parent; 16 return 0; 17 }else if(ptr->v==x){ 18 *last=ptr; 19 return 1; 20 }else if(ptr->v>x) 21 return BSTSearch(ptr->lc,x,ptr,last); 22 else 23 return BSTSearch(ptr->rc,x,ptr,last); 24 } 25 int BSTInsert(struct BSTTree *tp,int x){ 26 BSTNodePtr p,c; 27 if(!BSTSearch(tp->head,x,NULL,p)){ 28 ++tp->size; 29 c=malloc(sizeof(BSTNode)); 30 memset(c,0,sizeof(BSTNode)); 31 if(!p)tp->head=c; 32 else{ 33 if(p->v>x)p->lc=c; 34 else p->rc=c; 35 c->parent=p; 36 } 37 } 38 else return 0; 39 } 40 int BSTDelete(struct BSTTree *tp,int x){ 41 BSTNodePtr ptr,*parent; 42 if(!BSTSearch(tp->head,x,NULL,&ptr)) return 0; 43 else{ 44 if(tp->head->v==x){ 45 parent=&tp->head; 46 }else { 47 parent=&ptr->parent; 48 } 49 if(!ptr->lc){ 50 *parent=ptr->rc; 51 free(ptr); 52 } 53 else if(!ptr->rc){ 54 *parent=ptr->lc; 55 free(ptr); 56 }else{ 57 BSTNodePtr succ=ptr->lc; 58 while(succ->rc)succ=succ->rc; 59 succ->rc=ptr->rc; 60 ptr->rc->parent=succ; 61 *parent=ptr->lc; 62 free(ptr); 63 } 64 } 65 return 1; 66 }
查找,插入都没什么说的。
删除的时候是这样的,首先找到那个点(假设那个点存在)。
1,如果它是叶子,直接删除。
2,如果它只有一个孩子(一个子树),则让那个孩子直接取代它。(删除它,它的孩子接在它父亲的对应孩子的位置)
3,如果他有两个孩子,让他和自己的后继换位置,然后在它新的位置删除它。(新的位置一定没有左子树)
4,递归更新树的高度。
删除的另一个策略(主要针对第三步)是(程序写的是这个)
3,删掉它,让它左子树代替他的位置,然后把它右子树接在它的前驱的右孩子上。(它的前驱一定没有右孩子)
AVL树(国内很多教材也称之为平衡二叉树)
维护节点高度,维护左右子树高度差大于1的时候进行旋转操作来消除高度差。
旋转操作分为zig(右转)和zag(左转)
重平衡分为四种情况。
1,当前节点x的右孩子的右孩子过深,对x做一次zag操作。(单旋)
2,当前节点x的左孩子的左孩子过深,对x做一次zig操作。(单悬)
3,当前节点x的右孩子的左孩子过深,对x.rc做一次zig操作,对x做一次zag操作。(双旋)
4,当前节点x的左孩子的右孩子过深,对x.lc做一次zag操作,对x做一次zig操作。(双旋)
插入节点的时候,只会让一个祖先节点失衡,删除的时候,会递归的导致部分祖先都失衡,需要递归恢复平衡。
平衡二叉树:严格定义是两个子树高度差不超过特定常数的二叉树。AVL中这个常数是1。貌似这个概念最早在AVL树的论文中被提出,国内教材粗暴的把他们划上等号,实际上是不应该的。
平衡因子:就是左右子树高度差的别名,AVL中是1、0、-1
等价类问题
自反,对称,传递。
可以理解为不带缩短的并查集。
哈夫曼树
在叶子节点已经确定的情况下,一棵加权深度最小的树。
生成算法很简单。
1,取出当前最小的两个节点。
2,合并成一个新的节点并放回集合。
3,重复直到集合中只剩一个节点,它就是树根。
B-树
B-树读作B树(嗯,心里应该有B树),是一种平衡多叉树,经常用在文件系统中(多叉是为了在一个节点中存储更多信息,有效降低树高,减少IO,利用成块读取优化)
一个m阶B树,或为空,或满足以下要求:
1,树中每个节点至多有m个子树(每个节点至多有m-1个关键字)。
2,如果根节点不是叶子节点,则至少有2个子树。
3,每个除根节点以外的非终端节点有至少ceil(m/2)个子树。(ceil向上取整)
4,所有非终端节点包含有三种信息(关键字个数n,n个关键字k[1...n],n+1个子树指针p[0...n])(注意:n<m,n是关键字个数,m是最大子树个数,m-1是最大关键字个数)
5,所有叶子节点都在同一层次。
我在学习B树的时候,不是很明白为什么每个节点包含的子树数量除了上界还规定了下界,看完B树的插入删除操作也许就懂了。
B树的插入。
1,类似二叉搜索树,定位到它该在的已有节点,插入它。
2,如果这个节点包含的关键字个数等于m,则把当前节点的中位关键字提升至父节点,分裂当前节点。
3,因为提升操作,父节点关键字个数也可能等于m,对父节点进行同样的分裂提升操作,直到根节点。
4,如果一直提升,根节点也可能大于m,对根节点进行分裂提升,此时树高会+1,这是B树提升树高的唯一方式。(这也是为什么根节点可能只有两个子树的原因)
在这个过程中,每次分裂提升,保证B树的前三个性质。
接下来看看删除操作。
1,类似二叉搜索树,定位到它该在的已有节点,删除它。
2,如果删除的这个关键字在非叶子节点中,则用该关键字右侧子树中最小关键字替换(这个最小关键字一定在叶子上),删除完叶子节点关键字后,如果那个节点关键字个数大于等于ceil(m/2)-1,则删除完成。
3,叶子上被删除一个关键字后,可能会导致该节点关键字个数小于ceil(m/2)-1,等于ceil(m/2)-2,此时我们应该在他的左右兄弟看看能不能“借”一个节点,如果左右兄弟存在且关键字个数大于ceil(m/2)-1,那么我们可以借,具体做法是“旋转”,先把隔离两兄弟的父节点里面的对应关键字拿下来,再把兄弟的关键字提上去,完成后,父节点关键字数目不变,那个兄弟节点关键字数目-1,原始节点关键字数目不变。
4,如果左右兄弟都不能“借”,我们就要和兄弟合并,具体做法是从父节点拿到分隔两兄弟用的关键字,合并两兄弟(自己&左兄弟,或者,自己&右兄弟)。这时,有一定几率导致父节点关键字个数小于ceil(m/2)-1,继续34操作直到根节点。
5,如果根节点的关键字只剩一个,且它的两个儿子需要合并,于是整个树的高度就会下降,这是B树降低树高的唯一方式。
在这个过程中,每次合并,保证B树的前三个性质。
B+树
B+树是B-树的变型,他和B-树的不同在于。
1,有n个子树的节点有n个关键字。(B-树中,有n个子树的节点有n-1个关键字。)
2,叶子节点包含了全部关键字的信息,叶子节点自小到大依次顺序链接。(B-树中,叶子节点之间没有链接)
3,所有非终端节点可以看作是索引,节点中包含子树的最大关键字。(B-树中,非终端节点也是数据项)
B+树由于叶子节点用链式存储链接了起来,所以可以很好地支持顺序查找。
图论
最小生成树
prim算法:(这是堆优化版本的,如果不用堆,直接暴力的扫dis数组就行)(适用于稠密图)
1,初始一个空点集V,初始一个距离数组dis,表示V中元素到任意节点间的当前最短距离,初始化为正无穷,初始化一个空小根堆。
2,把任意一个点加进去V,用每一条这个点为起点的边更新dis数组,如果某一个dis被更新且目标点不在V中,我们就把这条边加入堆。
3,弹出堆中的最小值(一条边),如果这条边的目标点在V中,就重复3,直到堆空或得到一个目标点不在V中的边。
4,把目标点加入V,用每一条这个点为起点的边更新dis数组,如果某一个dis被更新且目标点不在V中,我们就把这条边加入堆。重复34,直到堆空或者sizeof(V)==N
kruskal算法
把所有的边放进小根堆,逐一弹出,如果两个点是一个集合则忽略,如果不是一个集合,则用这条边他们合并成一个集合。
所以,prim适合稠密图,kruskal适合稀疏图。
最短路
Dijkstra算法:
单源最短路,算法是这样的。
1,初始化距离数组dis为无穷表示源点目前到每个节点的最短距离。访问源点,并且更新dis数组。
2,在dis中找到值最小的,且没有访问过的节点,并且更新dis数组。重复2直到访问所有节点。
floyd算法:
求任意两点间最短路。(需要邻接表存储)
for(int k=1; k<=n; k++)
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)if(map1[i][j]>map1[i][k]+map1[k][j])
map1[i][j]=map1[i][k]+map1[k][j];
拓扑序
输出入度为0的点,然后删掉以这个点为起点的边(其实就是那些边的目标点的入度减一),循环做就行。
关键路径
AOE网络,边是活动,节点是事件。事件具有完成的先后顺序(就是有依赖关系),一个事件完成的标志,是指向它的所有活动均已结束。
路径长度最长的一条路,叫关键路径。长度是最短结束时间。
关键路径上的活动有一个特性,他们的最早开始时间和最迟开始时间相等。
我们可以按照拓扑有序算出每一个事件的最早发生时间ve,然后按照拓扑逆序算出事件的最迟发生时间vl。
然后可以计算每个活动edge的最早开始时间e=ve[edge.x],计算每个活动edge的最迟开始时间l=vl[edge.y]-edge.length
然后判断e是否==l
查找
静态查找表
折半查找
书上的写法。(mid如果没有命中,就没有继续看他的必要了,新的数组要舍弃mid)
BiSearch1 int BiSearch(int *a,int n,int x){ 2 //二分查找,数据存在a[1]到a[n],找不到返回0 3 int l=1,r=n; 4 while(l<=r){ 5 int mid=(l+r)/2; 6 if(a[mid]==x)return mid; 7 if(x<a[mid])r=mid-1; 8 else l=mid+1; 9 } 10 return 0; 11 }更多64种写法,可以参考这个链接 知乎:二分查找有几种写法,它们的区别是什么。LightGHLi的回答
针对通用二分的写法,我的理解是这样的,使用闭区间查找,l=1,r=n。由要求确定左右区间,如果是要最大的满足条件的i,那就是l=m||r=m-1。那就意味着需要向上取整。(否则m==l的时候会死循环)
索引查找
其实就是分块,每个块有自己的范围,块内无序,块间有序
动态查找表
树形
见上面二叉树章节的:BST,AVL,B-树,B+树
散列(hash)
hash主要由两部分组成:hash函数和冲突处理。(除留余数和二次探测是最常用的组合)
hash函数:
直接定址法:需要很多空间。。。取址范围小的时候可以用。
除留余数法:被除数应该是不大于hash表长度上限的最大的素数
数字分析法:取关键字的其中几位作为新的关键字。
平方取中法,为什么不取两边的数位?因为结果里中间的数字和几乎原数字的每一位都有关。
折叠法,把关键字分段,进行操作后(“不进位加”或者“异或xor”或者部分片段先“倒置”)得到的数字作为新的关键字
随机数法:利用系统自带的随机数生成器。关键字长度不等时使用。不推荐使用,因为跨平台性差。
冲突处理:
开放定址法:
线性探测再散列:+-1,+-2,+-3.。。。好处是数据靠近,方便读写,坏处是数据过于靠近,容易产生拥堵降低效率。
二次探测再散列:+-1,+-4,+-9.。。。i^2,好处是数据不会过于拥挤,坏处是使用这种方式的时候要注意容量上限的确定,必须是素数x,且满足x%4==3,否则只有50%的空间会被使用。
随机探测再散列:额。。。不推荐使用,因为跨平台性差。
再hash:费时
链地址:因为物理区域的不连续性,在连续访问冲突数据的时候需要大量IO。
公共溢出区:一旦溢出效率会奇低。。。。
hash表的删除
懒惰删除:并不是真的清空那个单元(因为清空的话,会让检索链断掉),而是在那个单元加个标记,表示“已被删除”。
在查找的时候,遇到这个标记就继续,插入的时候,遇到这个标记就直接插入。
关于二次探测处理冲突,有一些多余的东西要说说。
这里二次探测为什么双向进行是有原因的,在单向进行的时候,会有一个有趣的现象,对于一个起点,在容量为m的hash表中进行二次探测,m为合数的时候只能访问少于ceil(m/2)个互异的节点,m为素数的时候只能访问恰好ceil(m/2)个互异的节点。
使用单向的二次探测时,必须保证装填因子小于50%。能不能解决这个问题呢?于是提出了双向二次探测。
但是双向二次探测也有缺陷,在容量为m的hash表中进行双向二次探测,如果m%4==1,依然只能命中一半的空间。m%4==3的话,就可以命中所有空间,具体证明参考“学堂在线《数据结构》(清华大学 邓俊辉)第九章:词典 09D2-6 4K+3”
排序
选择排序、快速排序、希尔排序、堆排序是不稳定的排序算法,
而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法
有个很简单的判断是否稳定的方法,只要看看算法中是否涉及到交换操作,且交换的两个元素是不相邻的。
1 #include<stdio.h> 2 #define MAXN 20 3 //default order is not descending order 4 void init(int *a,int n,FILE *fp){ 5 int i; 6 for(i=1;i<=n;i++){ 7 fscanf(fp,"%d",&a[i]); 8 } 9 } 10 11 void PrintArray(int *a,int n){ 12 int i; 13 for(i=1;i<=n;i++){ 14 printf("%d ",a[i]); 15 } 16 printf("\n"); 17 } 18 19 void InsertSort(int *a,int n){ 20 //简单插入排序(稳定) 21 int i,j; 22 for(i=2;i<=n;i++){ 23 if(a[i]<a[i-1]){ 24 a[0]=a[i]; 25 a[i]=a[i-1]; 26 for(j=i-2;a[0]<a[j];j--){ 27 a[j+1]=a[j]; 28 } 29 a[j+1]=a[0]; 30 } 31 } 32 } 33 34 void BiInsertSort_book(int *a,int n){ 35 //折半插入排序(稳定) 36 int i,j; 37 for(i=2;i<=n;i++){ 38 int l=1,r=i-1; 39 a[0]=a[i]; 40 while(l<=r){ 41 int mid=(l+r)/2; 42 if(a[0]<a[mid])r=mid-1; 43 else l=mid+1; 44 } 45 for(j=i-1;j>=r+1;j--) 46 a[j+1]=a[j]; 47 a[r+1]=a[0]; 48 } 49 } 50 51 52 void ShellInsert(int *a,int dk,int n){ 53 int i,j; 54 for(i=dk+1;i<=n;i++){ 55 if(a[i]<a[i-dk]){ 56 a[0]=a[i]; 57 for(j=i-dk;j>0&&a[0]<a[j];j-=dk){ 58 a[j+dk]=a[j]; 59 } 60 a[j+dk]=a[0]; 61 } 62 } 63 } 64 void ShellSort(int *a,int n){ 65 //希尔排序(缩小增量排序)(不稳定) 66 int k,i; 67 int t=0,dlta[50]; 68 int tmp=n-1; 69 while((tmp>>=1)) 70 t++; 71 72 tmp=1; 73 for(k=0;k<=t;k++){ 74 dlta[t-k]=tmp; 75 tmp<<=1; 76 } 77 printf("%d\n",t); 78 PrintArray(dlta,t); 79 for(i=1;i<=t;i++){ 80 ShellInsert(a,dlta[i],n); 81 } 82 } 83 84 void BubbleSort(int *a,int n){ 85 //冒泡排序(稳定) 86 int i,j; 87 for(i=1;i<n;i++){ 88 for(j=1;j<=n-i;j++){ 89 if(a[j]>a[j+1]){ 90 a[0]=a[j]; 91 a[j]=a[j+1]; 92 a[j+1]=a[0]; 93 } 94 } 95 } 96 } 97 98 int Partition(int *a,int l,int r){ 99 a[0]=a[l]; 100 while(l<r){ 101 while(l<r&&a[r]>=a[0])r--; 102 a[l]=a[r]; 103 while(l<r&&a[l]<=a[0])l++; 104 a[r]=a[l]; 105 } 106 a[l]=a[0]; 107 return l; 108 } 109 void QuickSort2(int *a,int l,int r){ 110 if(l<r){ 111 int pivot=Partition(a,l,r); 112 QuickSort2(a,l,pivot-1); 113 QuickSort2(a,pivot+1,r); 114 } 115 } 116 void QuickSort(int *a,int n){ 117 //快速排序(不稳定) 118 QuickSort2(a,1,n); 119 } 120 121 void SelectSort(int *a,int n){ 122 //简单选择排序(不稳定) 123 int i,j; 124 for(i=1;i<n;i++){ 125 a[0]=i; 126 for(j=i+1;j<=n;j++){ 127 if(a[j]<a[a[0]]){ 128 a[0]=j; 129 } 130 } 131 if(a[0]!=i){ 132 int tmp=a[0]; 133 a[0]=a[i]; 134 a[i]=a[tmp]; 135 a[tmp]=a[0]; 136 } 137 //PrintArray(a,n); 138 } 139 140 } 141 142 void HeapAdjust(int *a,int x,int n){ 143 int tmp=a[x]; 144 int i; 145 for(i=2*x;i<=n;i<<=1){ 146 if(i<n&&a[i]<a[i+1])i++; 147 if(a[i]<=tmp)break; 148 a[x]=a[i];x=i; 149 } 150 a[x]=tmp; 151 } 152 void HeapSort(int *a,int n){ 153 //堆排序(不稳定) 154 int i; 155 for(i=n/2;i>0;i--){ 156 HeapAdjust(a,i,n); 157 } 158 for(i=n;i>1;i--){ 159 a[0]=a[1]; 160 a[1]=a[i]; 161 a[i]=a[0]; 162 HeapAdjust(a,1,i-1); 163 } 164 } 165 166 void SMerge(int *a,int l,int m,int r){ 167 int i; 168 int *b=calloc(r+1,sizeof(int)); 169 int p1=l,p2=m+1; 170 for(i=l;i<=r;i++)b[i]=a[i]; 171 for(i=l;p1<=m&&p2<=r;i++){ 172 if(b[p1]<=b[p2])a[i]=b[p1++]; 173 else a[i]=b[p2++]; 174 } 175 while(p1<=m)a[i++]=b[p1++]; 176 while(p2<=r)a[i++]=b[p2++]; 177 free(b); 178 } 179 void MSort(int *a,int l,int r){ 180 if(l<r){ 181 int m=(l+r)>>1; 182 MSort(a,l,m); 183 MSort(a,m+1,r); 184 SMerge(a,l,m,r); 185 } 186 } 187 void MergeSort(int *a,int n){ 188 //归并(递归实现)(稳定) 189 MSort(a,1,n); 190 } 191 192 int min(int a,int b){ 193 return a<b?a:b; 194 } 195 void MergeSort_book(int *a,int n){ 196 //二路归并(循环实现)(稳定) 197 int m,i; 198 for(m=1;m<n;m<<=1){//m<<=1 means m=m*2; 199 for(i=1;i<n;i+=m<<1){//i+=m<<1 means i=i+m*2; 200 if(i+m>n)continue; 201 SMerge(a,i,i+m-1,min(i+2*m-1,n)); 202 } 203 } 204 } 205 206 struct Ele{ 207 int x; 208 int tmp; 209 struct Ele *next; 210 }e[MAXN+5]; 211 struct Bucket{ 212 struct Ele *head; 213 struct Ele *tail; 214 }b[10]; 215 void RadixSortLSD(int *a,int n){ 216 //基数排序(稳定) 217 int flag=1,i; 218 struct Ele *p,*head=&e[1]; 219 for(i=1;i<=n;i++){ 220 e[i].x=a[i]; 221 e[i].tmp=a[i]; 222 e[i].next=&e[i+1]; 223 }e[n].next=NULL; 224 while(flag){ 225 flag=0;//用来判断下次基数排序是否继续 226 227 p=head;//基于低关键字放进对应的桶 228 while(p){ 229 int k=p->tmp%10; 230 if(p->tmp)flag=1; 231 p->tmp/=10; 232 if(!b[k].head) 233 b[k].head=p; 234 else b[k].tail->next=p; 235 b[k].tail=p; 236 p=p->next; 237 } 238 239 head=NULL;p=head;//从桶里依次取出来串成串 240 for(i=0;i<10;i++){ 241 if(b[i].head){ 242 if(!head) 243 head=b[i].head; 244 else p->next=b[i].head; 245 p=b[i].tail; 246 b[i].tail->next=NULL; 247 } 248 b[i].head=NULL; 249 b[i].tail=NULL; 250 } 251 } 252 253 p=head;i=1;//放回原始数组 254 while(p){ 255 a[i++]=p->x; 256 p=p->next; 257 } 258 } 259 260 void RadixSortMSD(int *a,int n){ 261 262 } 263 264 int main(){ 265 int a[MAXN+5],n; 266 FILE *fp; 267 fp=fopen("sort.in","r"); 268 269 fscanf(fp,"%d",&n); 270 init(a,n,fp); 271 272 //InsertSort(a,n); 273 //BiInsertSort_book(a,n); 274 //ShellSort(a,n); 275 //BubbleSort(a,n); 276 //QuickSort(a,n); 277 //SelectSort(a,n); 278 //HeapSort(a,n); 279 //MergeSort(a,n); 280 //RadixSortLSD(a,n); 281 MergeSort_book(a,n); 282 283 PrintArray(a,n); 284 return 0; 285 }
18 2 33 3 44 5 6 7 3 45 3 23 25 75 112 12 44 56 78
插入排序
针对于第i个元素,插入到[1...i-1]的有序序列,位置可以由折半查找logn确定,但是交换操作不能优化,所以不管是顺序插入还是折半插入,复杂度都是O(n)。
链式存储确实可以优化插入操作到O(1)但是不能对链式的定位是O(n)的,所以总的一次定位&插入的复杂度还是O(n)的。
希尔排序
插入排序,每个元素每次比较前进步长(增量)为1,而且,在序列基本有序的时候,复杂度会下降。于是希尔排序出现了,又叫缩小增量排序。
每趟希尔排序的时候规定一个增量dlta,每个元素不再和他的前一个元素比较了(a[i]<a[i-1]),而是和他之前距离为dlta的元素比较(a[i]-a[i-dlta])
dlta每趟结束后会递减,直到减到1。
最后一趟dlta为1,就变成了普通的插入排序,但是这时候序列已经基本有序了。
相比于插入排序,优化在“元素们在初期可以大步前进,而不是一次只前进1”。
希尔排序的复杂度在精心设计的dlta的辅助下,可以变成O(n*(logn)^2)
快速排序
每趟快排,以第一个元素为中枢(pivot),把比他大的放他左边,比他小的放他右边,这样他的位置就确定了,左右划分为两个子问题。递归解决就是。
它的操作是这样实现的,当pivot在左边时,从右往左扫,如果找到比中枢小的,就交换他们。然后从左往右扫,找到一个大的再交换,pivot就又到左边了。重复进行直到左右指针定位在一个位置。
每层的复杂度是O(n),平均logn层,总的就是O(nlogn)
他之所以被称为快速排序,因为在同数量级的排序算法中,他的平均时间复杂度的常数是最小的。
选择排序
选择排序是每次在剩余无序序列a[i...n]里,找到最小值,与序列第一个元素a[i]交换,然后序列长度减一变成a[i+1...n](此时,序列a[1...i]已经是有序的了)。
需要执行n次选择,每次选择需要遍历,所以复杂度是O(n^2)
注意这里跨度很大的交换操作,所以选择排序是不稳定的。
堆排
堆排是选择排序的优化,依然需要选择n次,但是有没有快速找出剩余元素最值的方法呢?有,借助数据结构,堆。
堆是这样定义的,
1,堆是一个完全二叉树,根节点是整个堆的最值。(根节点是最大值的堆叫大根堆,反之叫小根堆,我接下来都默认在说大根堆)
2,左子树右子树各是一个堆。
堆的插入很简单,
1,新元素放在末尾。(因为是完全二叉树,n的元素的堆的末尾是a[n])
2,如果当前元素大于自己的父亲,则两个节点交换元素。(a[n]>a[n/2])
3,重复进行2,直到根节点或者不大于自己的父亲则结束。
堆的删除也简单,
1,删除根节点,然后让根节点等于堆末尾的元素。
2,然后从根开始“调整”,如果当前元素不是左孩子&右孩子&自己中的最大值,则和最大值交换元素。
3,继续调整那个被影响的孩子,直到没有孩子了。
在排序中,不需要堆的插入,我们只要把已有数组调整成堆就行。
从n/2个节点向前,对每个节点为根的树进行调整(就是删除操作的23)。(因为后面n/2个节点自己是一棵树,只有一个节点的完全二叉树肯定是个堆啊不需要调整2333)
有了堆后,我们把无序数列调整成大根堆,然后取出最大值放在序列末尾,堆的容量减一,调整,重复这一操作。
取n次最值,每次取完后调整的复杂度是O(logn),总的时间复杂度是O(nlogn),建立的复杂度是O(nlogn)
归并排序
两个有序序列合并成一个有序序列,时间复杂度是O(n)
1,对于原始无序序列,我们可以划分为两个无序序列,然后继续划分,直到每个序列中只有一个元素,一个元素的序列已经是有序序列,然后开始合并。(这是书上代码演示的算法,也是我实现的)(递归做法)
2,一个有n个元素的无序序列,可以划分为n个一个元素的序列。对于每一个一个元素的序列,他们都是有序的,然后相邻的序列两两合并,重复这一做法,直到合并至整个序列。(这是书上文字描述的)(循环做法)(考试的时候9.9成考这个)
基数排序
不涉及交换,稳定的排序算法,按照高低位分桶优先级可以分为LSD和MSD两种。
LSD
从低位开始进行分桶,然后顺序链接(推荐使用链表实现),然后次低位分桶,然后链接,重复操作直到最高位,可以循环实现。
MSD
从高位开始,每一位进行分桶,在同一个桶内继续进行分桶,需要递归实现,消耗大量资源,不推荐使用。