数据结构

前排宣传邓俊辉老师的课程和课件

数据结构:

存储结构:顺序、链表、散列、索引。

逻辑结构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(后)

这就是前序遍历,中序遍历和后序遍历。(附上代码)

 1 #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.c
12
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
bi_tree.in

线索二叉树

 线索二叉树是由二叉树衍生出来的,普通二叉树结点只能保存孩子信息,如果想知道前驱后继,就比较麻烦(路径信息被浪费),于是衍生出了线索二叉树,可以存放前驱后继(按照某种遍历规则(前中后))。

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 }
BST

 查找,插入都没什么说的。

删除的时候是这样的,首先找到那个点(假设那个点存在)。

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)

 1 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 }
BiSearch

更多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 }
sort.c
18
2
33
3
44
5
6
7
3
45
3
23
25
75
112
12
44
56
78
sort.in

 插入排序

针对于第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

从高位开始,每一位进行分桶,在同一个桶内继续进行分桶,需要递归实现,消耗大量资源,不推荐使用。

 

posted @ 2017-10-12 11:26  徐王  阅读(1165)  评论(0编辑  收藏  举报