数据结构和算法

朴素模式匹配算法

1、循环模式串匹配目标字符串
最坏时间复杂度O(nm)

KMP算法

前提是模式串由部分相同子串
1、根据模式串求next数组(next[1]=0)
2、由next数组求目标字符串中是否存在模式串
平均时间复杂度O(n+m)
KMP优化:更具next数组求nextval数组,根据nextval数组进行匹配

树和二叉树

树的基本概念:任何一个结点,都有且仅有一个前驱结点,树是一种递归定义的数据结构
根结点、分支结点、边、叶子结点
空树:结点数为0的树
非空树:有且仅有一个根结点

结点之间的关系描述

祖先结点
子孙结点
双亲结点(父结点)
孩子结点:一个结点的直接后继结点
兄弟结点
堂兄弟结点
结点之间的路径
路径长度
结点、树的属性描述
结点的层次(深度):从上往下数
结点的高度:从下往上数
树的高度(深度):总共多少层
结点的度:有几个孩子(分支)
树的度:各各结点的度的最大值

有序树和无序树

有序树是树中结点的个个子树从左往右是有次序的,不能互换,无序树则无次序可以互换

森林

森林是m棵互不相交的树的结合(树和森林的相互转化问题),允许有空森林这样的状态存在
常见考点:
1、度为m的树和m叉树的存别
度为m的树:各各结点的度的最大值(树中结点最多有m个孩子,则度为m)。
m叉树的存别:每个结点最多只能有m个孩子的树,可以是空树。
有度的树一定是非空树,至少含有m个结点,m最小为0,即树只有一个根结点。
二叉树一定是三叉树、四叉树
2、度为m的树(m叉树等同)第i层至多有m^(i-1)个结点(i>=1)
3、高度为h的m叉树至多有(m^h - 1)/ (m-1)个结点
4、高度为h的m叉树至少有h个节点。
5、高度为h,度为m的树至少有h+m-1个结点
6、具有n个结点的m叉树的最小高度为logm(n(m-1)+1)

二叉树

二叉树是n个结点的有限集合:
1、或者是空二叉树,n=0;
2、或者由一个根结点和右互不相交的两个被称为左子树和右子树组成,左右子树分别是一颗二叉树
特点:1、每个结点至多有两棵子树;
2、左右子树不能颠倒(二叉树是有序树,即度为2的有序树)
几种特殊的二叉树:
满二叉树:高度为h,且含有2^h - 1个结点的二叉树。(一层1,二层2,三层4,四层8,......,2的指数级);
特点:
只有最后一层有叶子结点;
不存在度为1的结点;
按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为i/2向下取整。
完全二叉树,当且仅当其每个结点都与对应的满二叉树中编号为1-n的结点一一对应时称为完全二叉树。
只有最后两层可能有叶子结点;
最多只有一个度为1的结点;
按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为i/2向下取整;
i<=n/2为分支结点,大于为子结点。
二叉排序树,可以是一棵二叉树或者空二叉树,或者满足一下条件的二叉树。
左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有的结点的关键字均大于根结点的关键字;
左右子树又是分别满足上述条件的二叉排序树。
平衡二叉树,树上任意结点的左子树和右子树的深度之差不超过1,尽量追求平衡二叉树,能得到更高效的二叉排序树。

二叉树考点:
设非空二叉树中度为0,1,2的结点数分别为n0,n1,n2,则n0 = n2 + 1(叶子结点比二分支结点多一个);
二叉树的第i层最多有2(i-1)个结点(i>=1),对应的m叉树第i层至多有m(i-1)个结点(i>=1);

高度为h的二叉树最多有2^k - 1 个结点(满二叉树),高度为h的m叉树最多有(m^h - 1)/(m - 1)个结点(等比公式求和);
具有n个结点的完全二叉树的高度h为log2(n + 1) 向上取证或者log2n向下取整加1,第i个结点也满足上述结论;
对于完全二叉树,可以根据结点数n推出度数为0、1、2的结点个数n0,n1,n2;
完全二叉树最多只会有一个度数为0或者1的结点(满二叉树和完全二叉树的区别),故n1等于0或1;
n0 = n2 + 1,n0 + n2一定是奇数。
若完全二叉树有2k(偶数)个结点,则必有n1 = 1,n2 = k - 1,n0 = k;
若完全二叉树有2k - 1(奇数)个结点,则必有n1 = 0,n2 = k - 1,n0 = k。
二叉树存储结构
顺序存储(浪费空间、不直观)
链式存储(查找父节点不方便,只能从根结点遍历)
二叉树的遍历
根据跟的访问顺序分为先序、中序、后序(分支结点逐层展开);
如果只给出一棵二叉树的前中后层遍历序列中的一种是没办法唯一确定一棵二叉树的;
若果给出中序加上前序、后序、层序遍历序列中的一种,则可以确定唯一的二叉树(必须要含有中序);
前序 + 中序
后序 + 中序
层序 + 中序

先序遍历(根左右)

void PreOrder(BiTree T){
    if(T!=NULL){
        visit(T);
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}

中序遍历(左根右)

void CenOrder(BiTree T){
    if(T!=NULL){
        CenOrder(T->lchild);
        visit(T);
        CenOrder(T->rchild);
    }
}

后序遍历(左右根)

void LastOrder(BiTree T){
    if(T!=NULL){
        LastOrder(T->lchild);
        LastOrder(T->rchild);
        visit(T);
    }
}

算数表达式使用上述三种方式分别会得到前缀中缀后缀表达式。
层序遍历
使用队列,一层一层的访问,详细算法如下:

// 二叉树结点(链式存储)
typedef struct BiTNode {
    char data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BitTree;

// 链式队列结点
//////////////////////////////////////
typedef struct LinkNode{
    BiTNode * data;                 // 存指针,不是存结点
    struct LinkNode *next;
}LinkNode
typedef struct{
    LinkNode *front,*rear;          // 队头队尾
}
///////////////////////////////////////

// 层次遍历
void levelOrder(biTree T){
    LinkQueue Q;
    InitQueue(Q);                   // 初始化辅助队列
    BiTree p;
    EnQueue(Q,T);
    while(!IsEmpty(Q)){             // 队列不为空则循环
        DeQueue(Q,p);                // 对头结点出队
        visit(p);                   // 访问出队结点
        if(p->lchild!=NULL){
            EnQueue(Q,p->lchild);   // 左孩子入队     
        }
        if(p->rchild!=NULL){
            EnQueue(Q,p->rchild);   // 右孩子入队     
        }    
    }
}

遍历序列求二叉树
前序 + 中序

后序 + 中序

层序 + 中序

线索二叉树
将空链域变成线索
作用(线索二叉树用来解决下面的问题)
当给定结点不是根结点的时候没办法直接找到根结点
查找前驱后继节点不方便,遍历操作必须从根开始
中序线索二叉树(线索指向中序前驱、中序后继)

// 线索二叉树结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;   // 增加左右线索标志,tag==0表示指针指向孩子,tag==1表示指向的是线索
}ThreadNode,*ThreadTree;
*lchildltagdatartag*rchild

先序线索二叉树(线索指向先序前驱、先序后继)

后序线索二叉树(线索指向后序前驱、后序后继)

线索二叉树的前驱后继查找
中序线索二叉树
前驱:p的左子树的最右下结点
后继:p的右子树的最左下结点
先序线索二叉树
前驱
没有线索的情况下(ltag==0),没办法找到前驱(正常情况下);
如果能找到p的父节点,且p是左孩子,则p的父节点即为其前驱;
如果能找到p的父节点,且p是右孩子,其左兄弟为空,p的父节点即为其前驱;
如果能找到p的父节点,且p是右孩子,其左兄弟非空,p的前驱为左兄弟子树中最后一个被先序遍历的结点。
后继
若p有左孩子,则先序后继是左孩子;
若p没有左孩子,则先序后继为右孩子。
后序线索二叉树
前驱
若p有右孩子,则后序前驱为右孩子;
若p没有右孩子,则后序前驱是左孩子。
后继(能找到父节点(三叉链表查找父节点))
p是右孩子,p的父节点即为后继;
p是左孩子,右兄弟为空,p的父节点即为后继;
p是左孩子,其右兄弟非空,p的后继为右兄弟子树中第一个被后序遍历的结点;
如果p是根节点,则p没有后序后继。

存储结构
顺序存储
双亲表示法:每个结点中保存指向双亲的“指针”(位置下标)
查指定结点的双亲很方便;
查找孩子只能从头遍历,含有空数据遍历会更慢。
孩子表示法(顺序 + 链式存储)
找孩子很方便
不方便查找双亲
孩子兄弟表示法(链式存储)
用二叉链表存储树——左孩子右兄弟
孩子兄弟表示法存储的树从存储视角来看形态上和二叉树类似
森林和二叉树之间的转换
用二叉链表的方式存储森林:将森林中各个树的根结点视为兄弟关系
原则:右孩子表示兄弟结点;左孩子表示树的孩子(左孩子右兄弟)
树的遍历
核心递归算法实现遍历。
先根遍历(深度优先遍历):若树非空,先访问根结点,再依次对每棵子树进行先根遍历;
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
后根遍历(深度优先遍历):若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点;
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
层序遍历(广度优先遍历):队列实现
若树非空,则根结点入队;
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
重复b直到队列为空。
森林的遍历
递归思想遍历森林,根结点去掉之后,子结点会形成n个子森林。
先序遍历(若森林非空,则按照如下规则遍历)
访问森林第一棵树的根结点;
先序遍历第一棵树中根结点的子树森林;
先序遍历除去第一棵树之后剩余的树构成的森林。
中序遍历(若森林非空,则按照如下规则遍历)
中序遍历森林中第一棵树的根结点的子树森林;
访问第一棵树的根结点;
中序遍历除去第一棵树之后剩余的树构成的森林。
二叉排序树(BST)
二叉排序树的定义
二叉排序树又称为二叉查找树,简称BST(Binary Search Tree),具有如下性质:
左子树结点值 < 根结点值 < 右子树结点值
进行中序遍历可以得到一个递增的有序序列
查找操作
若树非空,目标值与根结点的值比较;
若相等,则查找成功;
若小于根结点,则在左子树上查找,否则在右子树上查找;
查找成功,返回结点指针;
查找失败返回NULL。
插入操作
若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。
不同的关键字序列可能得到同款二叉排序树,也可能得到不同的二叉排序树。
删除操作
先搜索找到目标结点:
若被删除结点是叶子结点,则直接删除,不会破坏二叉排序树的性质;
若删除的结点Z只有一棵左子树或右子树,则让z的子树成为z父节点的子树,替代z的位置;
若结点z有左右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
z的后继:z的右子树中最左下结点,该结点一定没有左孩子;
z的前驱:z的左子树中最右下结点,该结点一定没有右孩子。
查找效率分析
查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反应了查找操作时间复杂度。

查找成功的平均查找长度
ASL=(11 + 22 + 34 + 41)/8 = 2.625(上图平均长度结果)。
二叉排序树很大程度上取决于这棵树的高度是多少。
查找失败的平均查找长度ASL

ASL = (37 + 42 ) / 9 = 3.22
平衡二叉树(AVL)
定义
平衡二叉树简称平衡树——树上任意结点的左子树和右子树的高度差不超过1。
结点的平衡因子 = 左子树的高 - 右子树的高。
平衡二叉树结点的平衡因子的值只可能是-1,0,1。
平衡二叉树的结点数据类型:

typedef struct AVLNode {
    int key;          // 数据域
    int balance;      // 平衡因子
    struct AVLNode *lchild,*rchild;
} AVLNode,*AVLTree;

插入(插入后怎么保持平衡)
目标:
恢复平衡
保持二叉树特性
调整最小不平衡子树的方式如下:
LL:在A的左孩子的左子树中插入导致不平衡——右旋转

RR:在A的右孩子的右子树中插入导致不平衡——左旋转

LR:在A的左孩子的右子树中插入导致不平衡——先左旋后右旋

RL:在A的右孩子的左子树中插入导致不平衡——先右旋后左旋

平衡二叉树的查找效率分析
若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)
平衡二叉树最大深度为O(log n),平均查找长度/查找的时间复杂度为O(log n)
考点:高度为h的平衡二叉树最少有几个结点——递推求解
哈夫曼树(最优二叉树)
带权路径长度
结点的权:有某种现实含义的数值(如:表示结点的重要性);
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积;
树的带权路径长度:树中所有叶结点的带权路径长度之和,WPL,即i从1~n的Wili求和
哈夫曼树的定义
在含有n个带权叶结点的二叉树中,其中带权路径(WPL)最小的二叉树称为哈夫曼树,也称为最优二叉树
哈夫曼树的构造
给定n个权值分别为w1,w2,w3,...,wn的结点,构造哈夫曼树的算法描述如下:
从所有结点中选取两个权重最小的树作为新结点的左右子树,并将新的结点权值置为左右子树上根结点的权重之和;
从结点集合中删除刚才的两棵树,同时将新得到的树放入集合中;
重复2,3直到F中只剩下一棵树为止。
结论:
每个初始结点最终都会成为叶结点,且权值越小的结点到根结点的路径长度越大;
哈夫曼树的结点总数为2n-1;
哈夫曼树中不存在度为1的结点;
哈夫曼树并不唯一,但WPL必然相同且最优。
哈夫曼编码(非固定长度编码)
将字符频次作为字符结点权值,构造哈夫曼树,即可得哈夫曼编码,可用于数据压缩。
可变长度编码:允许对不同字符用不等长的二进制表示。
固定长度编码:每个字符用相等长度的二进制位表示。
前缀编码:若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。前缀编码解码无歧义。
由于哈夫曼树不唯一,所以哈夫曼编码不唯一。

图的存储(共四种)
邻接矩阵
0表示两个顶点之间相互不连接
1表示两个顶点之间相互连接

#define MaxVertexNum 100                    // 顶点数目的最大值
typedef struct{
    char Vex[MaxVertexNum];                 // 顶点表
    int Edge[MaxVertexNum][MaxVertexNum];   // 邻接矩阵,边表
    int vexnum,arcnum;                      // 图的当前顶点数和边数/弧数
}MGraph

结点的度
无向图
第i个结点的度 = 第i行(或第i列)的非零元素的个数(O(n));
有向图
第i个结点的出度 = 第i行的非零元素的个数(O(n));
第i个结点的入度 = 第i列的非零元素的个数(O(n));
第i个结点的度 = 第i行和第i列的非零元素的个数之和(O(n));
邻接矩阵的性能分析
邻接矩阵的性质
设邻接矩阵为A(矩阵元素为0/1),则A^n[i][j]等于由顶点i到顶点j的长度为n的路径的数目
空间复杂度:O(|V^2|)
适用于稠密图
表示方式唯一
计算出入度必须遍历行或列
找相邻的边需要遍历行或边
邻接表(顺序+链式存储)
各各结点顺序存储,在用各各结点的链表来存储边的信息;
只存储出的边;
邻接表表示方式不唯一;
计算有向图的度、入度和找有向图的入边,其他很方便;
只适用于存储稀疏图;
空间复杂度:无向图->O(|V| + |2E|),有向图->O(|V| + |E|);
十字链表(存储有向图)
空间复杂度:O(|V|+|E|)
邻接多重表(存储无向图)
空间复杂度:O(|V|+|E|)
图的基本操作
图的遍历
广度优先遍历
深度优先遍历
最小生成树
最短路径-BFS算法
最短路径-Dijkstra算法
最短路径-Floyd算法
有向无环图
拓扑排序
关键路径
查找
查找的基本概念
基本概念
查找:数据集合中寻找满足某种条件的数据元素的过程称为查找。
查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素或记录组成。
关键字:数据元素中唯一区分标示该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
如何评价一个查找算法
重点关注平均查找长度(ASL):查找成功,查找失败两种情况
顺序查找
算法思想
又称为线性查找,通常用于线性表(顺序、链式存储)
核心思想,从头到脚(从脚到头)挨个查找
算法实现
代码省略
查找成功
ASL(平均查找长度)= (n+1)/2
查找失败
ASL(平均查找长度)= n+1
算法优化
查找表中元素有序存放(递增 / 递减)(有序表),利用查找判定树进行查找;
查询概率不同,按照查询概率降序排列。
折半查找(二分查找)
算法思想
适用于有序的顺序表
算法实现

typedef struct {      //查找表的数据结构(顺序表)
    ElemType *elem;   // 动态数组基址
    int TableLen;     // 表的长度
}SSTable;

int Binary_Search(SSTable L,ElemType key){
    int low=0,high=L.TableLen-1,mid;
    while(low<=high){
        mid=(low+high)/2;
        if(L.elem[mid]>key){
            return mid;
        }else if(L.elem[mid]>key){
            high=mid-1
        }else{
            low=mid+1
        }
    }
    return -1
}

查找判定树
构造
有奇数个元素,mid分割后,刚好左右两边元素一样多;
有偶数个元素,mid分割后,左边比右边少一个元素(除2向下取整);
左子树节点数 - 右子树节点 = -1或0(不考虑失败节点);
折半查找判定树一定是平衡二叉树,只有最下面一层结点是不满的;
判定树满足二叉排序树定义;
判定树失败节点有n+1个(等于成功节点的空链域)。
折半查找效率(时间复杂度为O(log2n))
查找成功
最多四轮查找
ASL<=h
查找失败
最多四轮查找
ASL<=h
分块查找
B和B+树
B+树结点叶子结点个数与关键字相等
叶子结点包含所有的关键字,及其指向相应记录的指针
所有的分支结点仅包含下一级结点中的最大值,及其指向其子结点的指针
B+树查找每一次都是一条完整的从根结点到叶结点的路径
散列查找
概念:
散列表越长,冲突的概率越低,典型的用空间换时间的算法。
除留余数法——H(key)=key%p
散列表表长为m,取一个不大于m最接近于m的质数p,用质数取模,分布更均匀,冲突更少。
直接定址法——H(key)=key或H(key)=a*key+b
适合关键字分布基本连续的情况,若关键字不连续,空位较多,容易造成存储空间浪费。
数字分析法——选取数码分布较为均匀的若干位作为散列地址
平方取中法——取关键字平方值的中间几位作为散列地址
处理冲突的办法:
开发定址法
线性探测法——发生冲突时,每次往后探测相邻单元是否为空
平方探测法
伪随机序列法
拉链法(链地址法)——同义词链成一个链表
再散列法——准备多个散列函数,一个冲突就用另外一个
排序
排序算法的评价指标
时间复杂度
空间复杂度
算法稳定性(相同元素排序之后位置与排序前相同)
稳定的算法不一定比不稳定的好
分类
内部排序——数据都在内存中
时间复杂度
空间复杂度
外部排序——数据太多,无法全部放入内存中
时间复杂度
空间复杂度
磁盘读写次数
插入排序

//升序排序(不带哨兵)
void InsertSort(int A[],int n){
    int i,j,temp;
    for(i=1;i<n;i++){
        if(A[i]<A[i-1]){
            temp=A[i];
            for(j=i-1;j>=0&&A[j]>temp;--j){
                A[j+1]=A[j];
            }
            A[j+1]=temp;
        }
    }
}

//升序排序(带哨兵)
void InsertSort(int A[],int n){
    int i,j;
    for(i=1;i<n;i++){
        if(A[i]<A[i-1]){
            A[0]=A[i];
            // 不需要在判断j是否大于0
            // A[0]的时候相等就会退出循环,然后给A[1]赋值
            for(j=i-1;A[j]>A[0];--j){
                A[j+1]=A[j];
            }
            A[j+1]=A[0];
        }
    }
}

//折半查找优化
void InsertSort(int A[],int n){
    int i,j,low,high,mid;
    for(i=1;i<n;i++){
        if(A[i]<A[i-1]){
            A[0]=A[i];
            low=1;high=i-1;
            while(low<=high){
                mid=(low+high)/2;
                if(A[mid]>A[0]){
                    high=mid-1                
                }else{
                    low=mid+1                
                }
            }
            for(j=i-1;j>=high+1;--j){
                A[j+1]=A[j];
            }
            A[high+1]=A[0];
        }
    }
}

空间复杂度O(1)
时间复杂度考虑因素:主要来自于比较关键字、移动元素,若有n个元素,需要n-1趟处理
最好时间复杂度:O(n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
算法稳定性:稳定
优化:基于带哨兵插入排序,使用折半查找先查找插入位置,再移动元素
当low大于high时停止查找
A[mid]===A[0](出现相同元素)时,应该继续再mid所指位置右边寻找插入位置
链表的插入排序时间复杂度一样
希尔排序(Shell Sort)
思想:设置一个增量b,将原表拆分成大小为b的子表,对每个子表分别进行插入排序,然后缩小增量b,重复以上过程,直到d=1为止(先部分有序,再逐步全部有序)。对于增量b的设置,建议折半处理,每次将增量缩小一半
空间复杂度:O(1)
时间复杂度:无法算出时间复杂度,最坏时间复杂度为O(n^2)(增量d初始值等予1)
稳定性:不稳定
局限性:只能基于顺序表,不适用于链表
题型:给出增量d,分析出每一趟排序后的状态

void ShellSort(ElemType A[],int n){
    int dk,j,i;
    // A[0]只是暂存单元,不是哨兵。当j<=0时插入位置已到
    for(dk=n/2;dk>-1;dk=dk/2){
          for(i=dk+1;i<=n;i++){
              if(A[i]<A[i-dk]){     //将A[i]插入有序增量字表
                  A[0]=A[i];        // 暂存在A[0]
                  for(j=i-dk;j>0&&A[0]<A[j];j-=dk){
                      A[j+dk]=A[j]; //记录后移,查找插入位置
                  }
                  A[j+dk]=A[0]; // 插入
              }
          }
    }
}

交换排序
基于交换的排序,根据两个关键字的比较结果来对换两个记录在序列中的位置。包含冒泡排序和快速排序。
冒泡排序
从后往前两两对比相邻的元素,若为逆序,则交换两个元素,直至序列比较完,称为一趟冒泡排序
空间复杂度:O(1)
时间复杂度:
最好的情况(有序):O(n),比较次数n-1,交换次数0
最坏的情况(逆序):O(n^2),比较次数=交换次数=n(n-1)/2(每次交换都需要移动三次元素)
稳定性:稳定

// 交换
void swap(int &a,int &b){
    int temp = a;
    a = b;
    b = temp;
}

// 冒泡排序
void BubbleSort(ElemType A[],int n){
    int i,j;
    // 遍历n个元素
    for(i=0;i<n-1;i++){
         bool flag = false;
         for(j=n-1;j>1;j--){        // 一趟冒泡过程
             if(A[j-1]>A[j]){       // 若为逆序
                 swap(A[j-1],A[j]); // 交换
                 flag=true;
             }
         }
         // 只要其中一趟没有发生交换说明已经是有序的,然后中止循环
         if(flag==false){
            return;      // 本次遍历没有发生交换,说明表已经有序
         }
    }
}

快速排序
快速排序是所有内部排序算法中平均性能最优的排序算法
快速排序是不稳定算法
快速排序算法的性能主要取决于划分操作的好坏,严蔚敏版本的划分是以表中第一个元素作为枢轴来对表进行划分
算法效率
时间复杂度:O(n递归层数)
最好时间复杂度:O(n
log2n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n*log2n)
空间复杂度:O(递归层数)
最好空间复杂度:O(log2n)
最坏空间复杂度:O(n)
每一次选中枢轴将待排序列换分为很不平均的两个部分,会导致算法效率变低
若初始序列有序或逆序,则快速排序性能最差(每次选择的都是最靠右的元素)
408依次划分不是一趟排序
算法描述:代码描述

int Partition(ElemType A[],int low,int high){   // 一趟划分
    ElemType pivot=A[low]; // 将当前表中第一个元素设为枢轴,对表进行划分
    while(low<high){       // 循环跳出条件
        while(low<high&&A[high]>=pivot) --high;
        A[low]=A[high];    // 将比枢纽小的元素移动到左边
        while(low<high&&A[low]<=pivot) ++low;
        A[high]=A[low];    // 将比枢纽大的元素移动到右边
    }
    A[low]=pivot;          // 枢纽元素存放在最终位置
    return low;            // 返回枢纽存放的最终位置
}
void QuickSort(ElemType A[],int low,int high){
    if(low<high){
        int pivotpos=Partition(A,low,high);
        // 依次对两个字pivotpos进行递归排序
        QuickSort(A,low,pivotpos-1);   
        QuickSort(A,pivotpos+1,high); 
    }
}

选择排序
简单选择排序
每一趟排序在待排序元素中选取关键字最小的元素加入有序序列中
稳定性:不稳定
既可以用于顺序表也可以用于链表
堆排序
大根堆:根>=左、右孩子节点
基于大根堆得到的是递增序列
小根堆:根<=左、右孩子节点
基于小根堆得到的是递减序列
结点前置知识
i的左孩子2i
i的右孩子2i+1
i的父节点i/2向下取整
建立大根堆
将所有非终端节点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
在顺序存储的完全二叉树中,非终端结点编码i<=n/2向下取整
依次遍历非终端节点,对二叉树进行调整
检查当前结点是否满足根>=左右孩子,若不满足,将当前结点与更大的一个孩子互换(2i、2i+1中的最大值)
若元素互换破坏了下一级的堆,则采用相同的方法继续向下调整(小元素不断下坠)
代码实现:

// 建立大根堆
void BuildMaxHeap(int A[],int len){
    // 从后往前调整多有的非终端结点
    for(int i=len/2;i>0;i--){
        HeadAdjust(A,i,len);
    }
}
// 将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
    A[0]=A[k];       //A[0]暂存子树的根结点
    for(int i=2*k;i<=len;i*=2){ // 沿着key较大的子结点向下筛选
         if(i<len&&A[i]<A[i+1]){
             i++;     // 取key较大的子结点的下标
         }
         if(A[0]>=A[i]){
             break;   // 筛选结束
         }else{
             A[k]=A[i];  // 将A[i]调整到双亲结点上
             k=i         // 修改k值,以便继续向下筛选
         }
    }
    A[k]=A[0]         // 被筛选结点的值放入最终位置
}

基于大根堆进行排序
每一趟将堆顶元素加入有序子序列(与代排序列中的最后一个元素交换)
交换结束后将待排元素序列再次调整为大根堆(小元素不断下坠)

// 堆排序
void HeapSort(int A[],int len){
    BuildMaxHeap(A,len);      // 构建大根堆
    for(int i=len,i>1;i--){   // n-1趟的交换和构建大根堆
        swap(A[i],A[1]);      // 堆顶元素和堆底元素交换
        HeadAdjust(A,1,i-1);  // 把剩余的待排序元素整理成大根堆
    }
}

算法效率分析
总共需要n-1趟,每一次交换后都需要将根结点进行下坠调整
每一层排序复杂度不超过O(h) = O(log2n)
时间复杂度:nlog2n
空间复杂度:O(n)
稳定性:不稳定
若左右孩子相等,则优先和左孩子交换
归并排序
定义:把两个或多个已经有序的序列合并成一个
m路归并,没选出一个元素需要对比关键字m-1次
在内部排序中,一般采用2路归并
时间复杂度:O(n*log2n)
空间复杂度:O(n),来源与辅助数组B
稳定性:稳定
归并算法:

ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType)); // 定义辅助数组B
void Merge(ElemType A[],int low,int mid,int high){
    int i,j;
    // 将A的两段A[low...mid]和A[mid+1...high]各自有序,将他们合并成一个有序表
    for(int k=low;k<=high;k++){
        B[k]=A[k];                  // 将A中所有元素复制到B中
    }
    for(i=low,j=mid+1,k=1;i<=mid&&j<=high;k++){
        if(B[i]<=B[j]){             // 比较B中左右两段中的元素
            A[k]=B[i++];            // 将较小值复制到A中
        }else{
            A[k]=B[j++];        
        }
    }
    while(i<=mid) A[k++]=B[i++];    // 若第一个表未检测完,复制
    while(j<=high) A[k++]=B[j++];   // 若第二个表未检测完,复制
}
void MergeSort(ElemType A[],int low,int high){
    if(low<high){
        int mid=(low+high)/2;       // 对中间划分两个子序列
        MergeSort(A,low,mid);       // 对左侧子序列进行递归排序
        MergeSort(A,mid+1,high);    // 对右侧子序列进行递归排序
        Merge(A,low,mid,high);      // 归并
    }
}

基数排序
过程:
按照个位递减收集排序序列
按照十位递减收集排序序列,十位相同按照个位递减排列
按照百位递减收集排序序列,十位越大越先入队
基数排序通常基于链式存储实现。
空间复杂度:需要r个辅助队列,空间复杂度=O(r)
时间复杂度:一趟分配O(n),一趟收集O(r),总共d趟分配、收集,总的时间复杂度=O(d*(n+r))
稳定性:稳定
应用:(具体分析)
数据元素关键字可以很方便的拆分为d组,且d较小;(反例:给五个人身份证号排序)
每组关键字取值范围不大,即r较小;(反例:给中文人名排序)
数据元素个数n较大。(擅长:给一亿人身份证号排序)
外部排序
外部排序:数据元素太多,无法一次性全部读入内存进行排序,所以需要外部排序
外存与内存之间的数据交换
外部排序的原理
使用归并排序的方法,最少只需要在内存中分配三块大小的缓冲区即可对任意一个大文件排序
外部排序,使用三块内存区进行排序

posted @ 2023-03-02 15:50  沐雨辰沨  阅读(96)  评论(0编辑  收藏  举报