数据结构
本人的期末数据结构期末考试复习整理的知识点,把知识点理解一遍,有条件的话再结合书后习题练习一下(特别是二叉树和图论),成绩90+很轻松
数据结构的主要研究内容是非数值问题
数据:客观事务的符号表示,是所有能够输入计算机并被计算机程序处理的符号的总称
数据元素:数据的基本单位,在计算机中通常以一个整体进行考虑和处理
数据项:组成数据元素、有独立含义、不可分割的最小单位
数据对象:性质相同数据元素的集合,是数据的一个子集
数据结构:相互存在一种或多种特定关系的数据元素的集合
逻辑结构:从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的
逻辑结构的两个要素:数据元素和关系
逻辑结构的四类基本结构,集合结构、线性结构、树结构、图结构或网状结构
抽象数据类型:由用户定义的、表示应用问题的数学模型以及定义在这个模型上的一组操作的总称
抽象数据类型的三大部分:数据对象、数据对象上的关系集合、数据对象的基本操作集合
顺序存储结构,借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系
链式存储结构,为表示节点之间的关系,给每个节点附加指针字段,用于存放后继元素的存储地址
算法的五个重要特性,有穷性、确定性、可行性、输入、输出
算法的四个评判标准,正确性、可读性、健壮性、高效性
衡量算法效率的方法:事后估计法、事前分析估计法
不考虑计算机的软硬件等环境因素,影响算法时间代价的最主要因素是问题规模
时间复杂度,logn<n<nlogn<n^2<2^n;
算法的时间复杂度与问题的规模有关,还与待处理数据的初态有关
顺序表是随机存储的结构,链表是顺序存储的结构
顺序表的初始化
Status InitList(SqList &L){
L.elem = new ElemType[MaxSize];
if(!L) exit(OVERFLOW);
L.length = 0;
}
取值
Status GetElem(SqList L,int i, ElemType &e){
if(i<1||i>L.length) return ERROR;
e = L.elem[i-1];
return OK;
}
查找
int LocateElem(SqList L,ElemType e){
for(i=0;i<L.length;i++){
if(L.elem[i] == e) return i+1;
}
return 0;
}
插入
Status ListInsert(SqList &L,int i,ElemType e){
if(i<1 || i>L.length+1) return ERROR;
if(L.length == MaxSize) return ERROR;
for( j = L.length-1; j>=i-1;j--){
L.elem[j+1] = L.elem[j];
}
L.elem[i-1] = e;
L.length++;
return OK;
}
平均时间复杂度:2/n
删除
Status ListDelete(SqList &L,int i){
if(i<1 || i>L.length) return ERROR;
for(j=i;j<L.length;j++){
L.elem[j-1] = L.elem[j];
}
L.length--;
return OK;
}
平均时间复杂度:n-1/2
长度为N的顺序表中,插入一个新元素平均需要移动表中N/2个元素,删除一个元素平均需要移动N-1/2个元素
链表的初始化
Status InitList(LinkList &L){
L = new LNode;
L->next = null;
return OK;
}
取值
Status GetElem(LinkList L,int i,ElemType &e){
p = L->next;j=1;
while(p&&j<i){
p = p->next;
j++;
}
if(!p || j>i) return ERROR;
e = p ->data;
return OK;
}
查找
LNode *LocateElem(LinkList L,ElemType e){
p = L->next;j = 0;
while(p&&p->data!=e){
p= p->next;
}
return p;
}
插入
Status ListInsert(LinkList &L,int i,ElemType e){
p = L; j = 0;
while(p&&j<i-1){
p = p->next;
j++;
}
if(!p || j>i-1) return ERROR;
s = new LNode;
s->data = e;
s- >next = p - >next;
p->next = s;
return OK;
}
时间复杂度:O(n)
删除
Status ListDelete(LinkList &L,int i){
p =L; j = 0;
while(p->next&&j<i-1){
p = p->next;
j++;
}
if(!p || j>i-1) return ERROR;
q = p->next;
p->next = q->next;
delete q;
return OK;
}
时间复杂度 :O(n)
插入与删除的循环条件不同,是因为插入的合法位置有n+1个,删除的合法位置有n个,如果删除的循环条件与插入一致,会出现空指针的情况
前插法创建单列表 头结点与首元结点之间
void CreateList_H(LinkList &L,int n){
L = new LNode;
L->next = null;
for(i=0;i<n;i++){
p = new LNode;
cin>>p->data;
p ->next = L->next;
L->next = p;
}
}
后插法创建单链表 所有节点之后
void CreateList_R(LinkList &L,int n){
L = new LNode;
L ->next = null;
r = L;
for(i = 0;i<n;i++){
cin>>p->data;
p->next = null;
r ->next = p;
r = p;
}
}
顺序表的存储密度为1,链表的存储密度小于1。
进行存取操作考虑顺序表,进行插删操作考虑链表。
顺序表的合并
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
LC.length = LA.length+LB.length;
LC = new ElemType[LC.length];
pc =LC.elem, pa = LA.elem, pb = LB.elem;
pa_last = LA.elem+LA.length-1;
pb_last = LB.elem+LB.length-1;
while(pa<=pa_last && pb<=pb_last){
if(*pa<=*pb){ *pc++=*pa++; }
else *pc++=*pb;
}
while(pa<=pa_last){*pc++=*pa;}
whilel(pb<=pb_last){*pc++=*pb;}
}
链表的合并
void MergeList_L(LinkList &LA, LinkList &LB, LinkList &LC){
LC = LA;//直接合并到LA后面
pc = LC, pa = LA->next, pb = LB->next;
while(pa&&pb){
if(pa->data<=pb->data){ pc->next = pa, pc = pa, pa=pa->next}
else pc->next = pb, pc = pb, pb=pb->next;
pc ->next = pa?pa:pb;
delete LB;要把LB的头结点删掉
}
}
比较项目 | 顺序表 | 链表 |
存储空间 | 预先分配,容易造成空间闲置或溢出 | 动态分配,不会溢出或闲置 |
存储密度 | 等于1 | 为表示节点之间的逻辑关系而增加额外的存储开销,小于1 |
存储元素 | 随机存储,访问时间复杂度O(1) | 顺序存储,访问时间复杂度O(n) |
插入、删除 | 时间复杂度O(n) | 时间复杂度O(1) |
使用情况 | 长度变化不大,不经常使用插删操作 | 长度变化较大,经常进行插删操作 |
顺序栈的初始化
Status InitStack(SqStack &S){
S.base = new SElemType[MaxSize];
if(!S.base) exit(OVERFLOW);
S.top = S.base;
S.stackSize = MaxSize;
return OK;
}
入栈
Status Push(SqStack &S, SElemType e){
if(S.top-S.base==MaxSize) return ERROR;
*S.top = e;
S.top++;
return OK;
}
出栈
Status Pop(SqStack &S,SElemType &e){
if(S.top==S.base) return ERROR;
e = *--S.top;
return OK;
}
取栈顶
SElemType GetTop(SqStack S, SElemType &e){
if(S.base!=S.top){ return *(S.top-1);}
}
链栈的初始化
Status InitStack(LinkStack &S){
S = null;
return OK;
}
入栈 头插,连接到栈顶上方,且链栈没有设置栈顶与栈底指针
Status Push(LinkStack &S, SElemType e){
p = new StackNode;
p ->data = e;
p->next = S;
S = p;
return OK;
}
出栈
Status Pop(LinkStack &S , SElemType &e){
if(S==null) return ERROR;
e = S->data;
p = S;
S = S->next;
delete p;
return OK;
}
取栈顶
SElemType GetTop(LinkStack S, SelemType &e){
if(S!=null) return S->data;
}
汉诺塔算法(要能够掌握递归的过程,有考题会让画递归调用图)
void move(char A,int n,char C){
count<<n<<","<<A<<C;
}
void Hanoi(int n, char A,char B, char C){
if(n==1) move(A,1,C);
else{
Hanoi(n-1,A,C,B);
move(n,A,B,C);
Hanoi(n-1,B,A,C);
}
}
顺序队列的初始化 队首与队尾都是整数
Status InitQueue(SqQueue &Q){
Q.base = new QElemType[MaxSize];
if(!Q.base) exit(OVERFLOW);
Q.front = Q.rear = 0;
return OK;
}
入队 对于循环队列的队首与队尾指针改变都需要对队长取余
Status EnQueue(SqQueue &Q, QElemType e){
if((Q.rear+1%MaxSize)==Q.froznt) return ERROR;
Q.base[Q.rear] = e;
Q.rear=(Q.rear+1)%MaxSize;
return OK;
}
出队
Status DeQueue(SqQueue &Q,QElemType &e){
if(Q.rear == Q.front) return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front+1)%MaxSize;
return OK;
}
取队首
QElemType GetHead(SqQueue Q,QElemType &e){
if(Q.front!=Q.rear){
return Q.base[Q.front];
}
}
链式队列的初始化 front不存储,入栈直接连接到队尾后
Status InitQueue{
Q.front = Q.rear = new QNode;
Q.front ->next =null;
return OK;
}
入队 尾插
Status EnQueue(LinkQueue &Q, QElemType e){
p = new QNode;
p->data = e;
p->next = null;
Q.rear->next = p;
Q.rear = p;
return OK;
}
出队
Status DeQueue(LinkQueue &Q, QElemType &e){
if(Q.rear==Q.front) return ERROR;
p = Q.front->next;
e = p->data;
Q.front->next = p->next;
if(Q.rear = p) Q.rear = Q.front;//删除了最后一个元素
delete p;
return OK;
}
取队首
QElemType GetHead(LinkQueue Q, QElemType &e){
if(Q.rear!=Q.front) {
return Q.front->next->data;
}
}
进制转换
void converion(int N){
InitStack(S);
while(N){
Push(S,N%8);
N/=8;
}
while(!StackEmpty(S)){
Pop(S,e);
count<<e;
}
}
括号匹配
Status Matching(){
InitStack(S);
flag = 1;
cin>>ch;
while(ch!='#'&&flag){
Swith(ch){
case '[':
case '{':
Push(S,ch);
break;
case ']':
if(!StackEmpty(S)&&GetTop(S)=='['){
Pop(S,e);
}
else flag = 0;
break;
case '}':
if(!StackEmpty(S)&&GetTop(S)=='}'){
Pop(S,e);
}
else flag = 0;
break;
}
}
if(flag&&StackEmpty(S)) return true;
else return false;
}
队列应用于打印机、模拟排队场景
堆可以为每一个新产生的串动态分配一块实际串长需要的存储空间
模式匹配算法 BF算法(牢记i指针和i指针的回溯位置)
int Index_BF(SString S, SString T, int pos){
i = pos, j =1;
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.ch[j]) i++, j++;
else{
i = i-j+2;
j = 1;//匹配失败,重新追溯
}
}
if(j>T.length) return i-T.length;//子串匹配完成,成功
else return 0;
}
最好情况时间复杂度,O(n+m)
最坏时间复杂度,O(n*m)
时间复杂度,O(m*n)
求子串的个数=n*(n+1)/2+1
低下标优先优先存储类似行优先存储,高下标优先存储类似于列优先存储
对称矩阵,以行优先为例(具体考虑是从1开始存储还是0开始存储,0开始存储需要减1,1开始存储不需要减1)
i*(i-1)/2+j-1 | i>=j | 下三角 |
j*(j-1)/2+j-1 | i<j | 上三角 |
三角矩阵,从1开始
(i-1)*(2n-i+1)/2+(j-i) | i<=j | 上三角矩阵 |
0或n*(n+1)/2 | i>j | 上三角矩阵 |
i*(i-1)/2+j-1 | i>=j | 下三角矩阵 |
0或n*(n+1)/2 | i<j | 下三角矩阵 |
广义表的长度是指表中子表个数,深度指子表中元素的最大个数
广义表的表头可能是一个单原子,也可能是一个子表;表尾一定是一个广义表
计算规则(牢记),根据节点数计算二叉树有多少种=(1/n+1)*C2n n
二叉树的第n层最多为2^(n-1)个,深度为k的二叉树至多有(2^k)-1个节点,证明过程
总节点数N=n0+n1+n2,其中度0节点比度2节点多一个,即n0=n2+1,所以N=2*n2+n1+1,证明过程
具有n个节点的完全二叉树的深度为(log 2 n)+1,证明过程
顺序存储结构仅适用于完全二叉树,对于一般的二叉树采用的是链式存储方式
含有n个节点的二叉链表中有n+1个空链域
中序遍历
void InOrderTraverse(BiTree T){
if(T){
InOrderTraverse(T->lchild);
count<<T->data;
InOrderTraverse(T->rchild);
}
}
中序遍历,非递归算法
void InOrderTraverse(BiTree T){
InitStack(S),p = T;
q = new BitNode;//用于暂时存放栈顶弹出的元素
while(p || !StackEmpty(S)){
if(p){
Push(S,p);
p = p->lchild;
}
else{
Pop(S,q);
count<<q->data;
p = q->rchild;//访问右子树
}
}
}
递归与非递归遍历二叉树,无论采用哪一种次序进行遍历,含n个节点的二叉树,其时间复杂度为O(n),空间复杂度也为O(n)
二叉树的先序与中序,或中序与后序能唯一地确定一颗二叉树
先序创建二叉链表,递归
void CreateBiTree(BiTree T){
cin>>ch;
if(ch=='#'){
T = NULL;
}
else{
p = new BiTNode;
p->data = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
复制二叉树,递归
void Copy(BiTree T, BiTree &NewT){
if(!T){
NewT = NULL;
return;
}
esle{
NewT = new BiTNode;
NewT->data = T->data;
Copy(T->lchild,NewT->lchild);
Copy(T->rchild,NewT->rchild);
}
}
计算深度,递归
int Depth(BiTree T){
if(T==NULL) return 0;
else{
m = Depth(T->lchild);
n = Depth(T->rchild);
if(m>n) return (m+1);
else return (n+1);
}
}
统计各节点个数
int NodeCount(BiTree T){
if(T==NULL) return 0;
else{
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
}
int SingleNode(BiTree T){
if(T==NULL) return 0;
if(T->lchild==NULL&&T->rchild!=NULL || T->lchild!=NULL&&T->rchild==NULL) return 1;
else{
m = SingleNode(T->lchild);
n = SingleNode(T->rchild);
num = m+n;
}
return num;
}
int LeafNode(BiTree T){
if(T==NULL) return 0;
if(T->lchild==NULL && T->rchild==NULL) return 1;
else{
m = LeafNode(T->lchild);
n = LeafNode(T->rchild);
num = m + n;
}
return num;
}
int DoubleNode(BiTree T){
if(T==NULL || (!T->rchild && !T->lchild)) return 0;
else{
if(T->rchild && T->rchild){
num++;//只要不是空或者、单分支、度0节点
num+ =DoubleNode(T->lchild);
num+ =DoubleNode(T->rchild);
}
}
return num;
}
树的先根和后根序列遍历依次对应先序与中序遍历;森林的先序和中序遍历依次对应先序与中序遍历
构造哈夫曼树,n个叶节点的哈夫曼树共有2n-1个节点
初始化:首先申请2n个单元,将所有单元的双亲,左孩子,右孩子的下标初始化为0,最后循环n次输入前n个单元的叶子的权重;
创建:通过n-1次的选择、删除、合并
选择是选择双亲为且权重最小的两个树根节点s1和s2;
删除是将s1,s2的双亲改为非0;
合并是将s1,s2的权值之和作为一个新的节点存入数组n+1号及之后的单元中,同时记录这个节点的左孩子与右孩子是s1,s2
void CreateHuffmanTree(HuffmanTree &HT, int n){
if(n<=1) return;
m = 2*n-1;
HT = new HTNode[m+1];//0号单元未使用
for(i = 1;i<=m;i++){HT[i].parent = 0, HT[i].lchild = 0, HT[i].rchild = 0;}
for(i = 1;i<=n;i++){cin>>HT[i].weight;}
for(i = n+1;i<=m;i++){
Select(HT,i-1,s1,s2);
HT[s1].parent = i, HT[s2].parent = i;
HT[i].lchild = s1, HT[i].rchild = s2;
HT[i].weight = HT[s1].weight+HT[s2].weight;
}
}
哈夫曼编码是前缀编码,是最优前缀编码
所有点的度数和 = 2*边数和
n个顶点的无向图最多有 n*(n-1)/2条边
n个顶点的有向图·最多有 n*(n-1)条边
连通图的连通分支量为 0
有向树,有一个顶点入度为,其余顶点的入度均为1的有向图
邻接矩阵的存储结构
typedef struct{
VerTexType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum,arcnum;//顶点数,边数
}AMGraph;
邻接矩阵表示法创建无向网
Status CreateUDN(AMGraph G){
cin>>G.vexnum>>G.arcnum;
for(i=0;i<G.vexnum;i++){
cin>>G.vexs[i];
}
for(i=0;i<G.vexnum;i++){
for(j=0;j<G.vexnum;j++){
G.arcs[i][j] = MaxInt;
}
}
for(k=0;k<G.arcnum;k++){
cin>>v1>>v2>>w;
i = LocateVex(G,v1), j = LocateVex(G,v2);
G.arcs[i][j] = w;// 如果是图,只需要赋值为1
G.arcs[j][i] = G.arcs[i][j];//如果是有向,则不需要对称复制
}
return OK;
}
对于无向图,第i行就是Vi的度;对于有向图,第i行是Vi的出度,第i列是Vi的入度
一个图的邻接图唯一,但邻接表不唯一,邻接表中各节点的链接次序取决于邻接表的算法还有边的输入顺序
邻接矩阵表示的优缺点(对比着记忆0)
优点 | 便于判断两个顶点之间是否有边;便于计算各顶点的度 |
缺点 | 不便于增加与删除顶点;不便于统计边的条数;空间复杂度更高 |
邻接表表示的优缺点
优点 | 便于增加和删除顶点;便于统计边的条数;空间利用率更高 |
缺点 | 不便于判断顶点之间是否有边;不便于计算顶点的度数 |
深度优先搜索——树的先序遍历的推广,是一个递归的过程,借助栈
采用邻接矩阵,深度优先搜索遍历
void DFS(Graph G, int v){
visited[v] = true;
cout<<G.vexs[v];
for(w = 0;w<G.vexnum;w++){
if(G.arcs[v][w]!=0 && G.arcs[v][w]!=MaxInt && !visited[w]){
DFS(G,w);
}
}
}
采用邻接表,深度优先搜索遍历
void DFS(ALGraph G, int v){
visited[v] = true;
cout<<G.vertices[v].data;
for(p = G.vertices[v].firstarc;p;p=p->nextarc){
w = p->adjvex;
if(!visited[w]) DFS(G,w);
}
}
广度优先遍历——树的层次遍历,借助队列
以邻接表为例
void BFS(Graph G, int v){
cout<<v,visited[v] = true;
InitQueue(Q);
EnQueue(Q,v);
while(!QueueEmpty(Q)){
DeQueue(Q,u);
for(p = G.vertices[u].firstarc;p;p=p->nextarc){
w = p->adjvex;
if(!visited[w]){
cout<<w;
visited[w] = true;
EnQueue(Q,w);
}
}
}
}
深度优先搜索和广度优先搜索的时间复杂度一样,当用邻接矩阵存储时,时间复杂度为O(n^2);采用邻接表存储的时候,时间复杂度是O(n+e),两种遍历的不同在于对顶点的访问顺序不同
图的应用
最小生成树,各边代价之和最小的那颗生成树
prim算法——加点法
时间复杂度O(n^2),与边数无关,空间复杂度O(n),适用于求稠密图的最小生成树
kruscal算法——加边法
时间复杂度O(elog2e),与边数有关,适用于求稀疏图的最小生成树
最短路径
Dijkstra算法
时间复杂度O(n^2)
Floyd算法
时间复杂度O(n^3),空间复杂度O(n^2)
拓扑排序
AOV-网,以顶点表示活动,用弧表示活动间的优先关系的有向图
选择无前驱的顶点,删除该顶点以及与它有联系的弧,在剩下的节点重复操作,输出的顶点如果少于总顶点数,则说明有环,否则没有环
对于给出的AOV网判断网中是否存在环,对有向图的所有顶点进行拓扑排序,如果所有的顶点都在拓扑序列中,那么不存在环
时间复杂度O(n+e)
关键路径,源点到汇点的带权路径长度最长的路径
AOE-网,以边表示活动的网
时间复杂度O(n+e)
查找(记忆各个查找算法的特点)
顺序查找 | 折半查找 | 分块查找 | 二叉排序树查找 | |
优点 | 对表结构没有要求,使用顺序,也使用链式 | 比较次数少,查找效率高 | 在表中进行插删操作时,只需要找到对应的块就可以在块内进行插删操作,如果线性表经常动态变化,又需要快速查找,则使用分块查找 | 对于经常插入,删除,和查找运算的表,采用二叉排序树更好 |
缺点 | 平均查找长度较大,查找效率低 | 对结构要求高,只能适用于顺序存储结构的有序表,不适用数据元素经常变动的线性表 | 要增加一个索引表的存储空间并对初始索引表进行排序运算 | |
平均查找长度 时间复杂度 | (n+1)/2 O(n) | 最坏情况查找不超过log2n+1 与二叉排序树的一样 O(log2n) | 1/n * (每层个数*该层层数) O(log2n) |
顺序查找
平均查找长度ASL = 1/n * (n*(n+1)/2),时间复杂度O(n)
优点 | 对表结构没有要求,使用顺序,也使用链式 |
缺点 | 平均查找长度较大,查找效率低 |
折半查找
要求线性表必须采用顺序存储结构,最好情况查找1次,最坏情况查找log2(n)+1次,平均查找长度ASL=log2(n+1)-1
优点 | 比较次数少,查找效率高 |
缺点 | 对结构要求高,只能适用于顺序存储结构的有序表,不适用数据元素经常变动的线性表 |
分块查找
优点 | 在表中进行插删操作时,只需要找到对应的块就可以在块内进行插删操作,如果线性表经常动态变化,又需要快速查找,则使用分块查找 |
缺点 | 要增加一个索引表的存储空间并对初始索引表进行排序运算 |
二叉排序树的查找
含有n个节点的二叉排序树的平均查找长度和树的形态有关,最坏情况二叉树的形态为单分支形态,O(log2n)
ASL = 1/n*(1+2+...+n)
对于经常插入,删除,和查找运算的表,采用二叉排序树更好
void SearchBST(BSTree T, KeyType key){
if(!T || key==T->data.key) return T;
else if(key<T->data.key) return SearchBST(T->lchild,key);
else return SearchBST(T->rchild,key);
}
二叉排序树的插入
void InsertBST(BSTree &T, ElemType e){
if(!T){
s = new BSTNode;
s->data = e;
s->lchild=NULL, s->rchild = NULL;
T = s;
}
else if(e.key<T->data.key) return InsertBST(T->lchild,e);
else if(e.key>T->data.key) return InsetBST(T->rchild,e);
}
时间复杂度同查找一样,O(log2n)
二叉排序树的创建
void CreateBST(BSTree &T){
//依次读入关键字为key的节点,把相应的节点插入到二叉排序树中
T= NULL;
cin>>e;
while(e.key!='#'){//输入结束标志
InsetBST(T,e);
cin>>e;
}
}
插入一个节点的时间复杂度是O(log2n),n个节点的时间复杂度是O(nlog2n)
二叉排序树的删除
基本过程也是查找,所以时间复杂度是O(log2n),采用的过程是找到待删节点的直接前驱节点p,将p代替待删节点,将p的左子树接到p的双亲的右子树上面
平衡二叉树,AVL树
左子树与右子树的深度之差绝对值不超过1;左子树与右子树也是平衡二叉树
深度与log2n是同一数量级,所以查找的时间复杂度O(log2n)
散列表查找法,无需作比较或做很少比较,按照这种关系直接由关键字找到相对应的记录
散列函数和散列地址,在记录的存储地址p和关键字key之间建立一个确定的对应关系H,使得p=H(key),称这个对应关系H是散列函数,p是散列地址
散列表,一个连续的地址空间,用以存储按照散列函数计算得到的相应的散列地址的数据记录
冲突与同义词,对不同的关键字可能得到同一个散列地址,这种现象叫做冲突;具有相同函数值的关键字对该散列函数而言是同义词
处理冲突的方法,开放地址法,链地址法
散列函数的构造方法,考虑因素
散列表的长度
关键字的长度
关键字的分布情况
计算散列函数所需的时间
记录的查找频率
装填因子α越小,冲突发生频率越小,反之越大;散列函数均匀的情况下,影响平均查找长度的因素——处理冲突的方法和装填因子α,而不是待存放元素个数
散列表平均查找长度
插入排序(看后面的对比)
直接插入排序,时间复杂度O(n^2),空间复杂度,需要借助一个监视哨,O(1)
算法稳定、简单,适用于链式存储结构,顺序存储结构;更适用于初始记录基本有序的情况,初始记录无序,n较大时,则不适用
void InsertSort(SqList &L){
for(i=2;i<=L.length;i++){
if(L.r[i].key<L.r[i-1].key) {
L.r[0].key = L.r[i].key;
L.r[i] = L.r[i-1];
for(j=i-2; L.r[0].key<L.r[j].key ;j--){
L.r[j+1] = L.r[j];
}
L.r[j+1] = L.r[0];//比较到比插入的值还要小的数时,又往前面走了一个单位,所以要j+1
}
}
}
折半插入排序
时间复杂度O(n^2),空间复杂度O(1);只适用于顺序结构,不适用链式结构,适用于初始无序,n值较大的情况
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+1] = L.r[j];//后移
L.r[high+1] = L.r[0];//替换
}
}
希尔排序
记录跳跃式地移动导致排序是不稳定的,只能适用于顺序结构,不能使用链式结构,适用于初始记录无序,n值较大的情况
冒泡排序
void BubbleSort(SqList &L){
m = L.length-1,flag = 1;
while(m>0 && flag==1){
flag = 0;//默认本趟没有发生了交换
for(j = 1;j<=L.length;j++){
if(L.r[j+1]<L.r[j]){
flag = 1;//发生了交换做的标志
temp = L.r[j+1];
L.r[j+1] = L.r[j];
L.r[j] = temp;
}
}
m--;
}
}
时间复杂度O(n^2),空间复杂度O(1);稳定排序,同样适用于链式结构,移动次数多,比直接插入排序差;不适用初始无序,n值较大的情况
快速排序
int Partition(SqList &L, int low, int high){
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while(low<high){
while(low<high && L.r[high].key>=pivotkey) --high;//从右往左开始,目的是寻找比枢轴变量更小的量
L.r[low] = L.r[high];
while(low<high && L.r[low].key<=pivotkey) low++;//从左往右开始,目的是寻找比枢轴变量更大的量
L.r[high] = L.r[low];
}
//枢轴变量到位
L.r[low] =L.r[0];
return low;
}
void QSort(SqList &L, int low, int high){
//调整low=1, high = L.length
if(low<high){
pivotloc = Partition(L,low,high);
QSort(L,low,pivotloc-1);
QSort(L,pivotloc+1,high);
}
}
void QuickSort(SqList &L){
QSort(L,1,L.length);
}
时间复杂度O(nlog2n),空间复杂度,因为调用是递归的,需要使用工作栈,O(n);适用于顺序结构,很难适用链式结构、适用初始记录无序,n较大的情况
选择排序、一次选择最小的记录
void SelectSort(SqList &L){
for(i=1;i<L.length;i++){
k =i;
for(j =i+1;j<=L.length;j++){
if(L.r[j].key<L.r[k].key) k = j;
}
if(k!=i){
temp = L.r[i];
L.r[i] = L.r[k];
L.r[k] = temp;
}
}
}
时间复杂度O(n^2),空间复杂度O(1)、算法不稳定、可适用于链式结构、顺序结构
堆排序
不稳定排序,只适用顺序结构,不适用链式结构;
二路归并排序
时间复杂度O(nlog2n),空间复杂度O(n),是稳定排序,适用顺序结构、链式结构,工作时仍然需要开辟工作栈
(要掌握每个算法走一趟后的序列,不需要掌握具体的算法,自己会一两个就行,重要的是排序算法的过程,还有各自的特性)
直接插入排序 | 时间复杂度O(n^2),空间复杂度O(1) 适用于初始记录有序的情况,不适用初始记录无序,n较大的情况 适用顺序结构,也适用链式结构 | 稳定 |
折半插入排序 | 时间复杂度O(n^2),空间复杂度O(1);适用初始无序,n较大的情况;适用顺序结构,不适用链式结构 | 稳定 |
希尔排序 | 时间复杂度O(n^1.3),空间复杂度O(1) 适用于初始记录无序,n较大的情况 只适用顺序结构,不适用链式结构 | 不稳定 |
冒泡排序 | 时间复杂度O(n^2),空间复杂度O(1) 不适用初始记录有无序,n较大的情况 适用顺序结构、链式结构 | 稳定 |
快速排序 | 时间复杂度O(nlog2n),空间复杂度O(n) 适用初始记录无序,n较大的情况 适用顺序结构,很难适用链式结构 | 不稳定 |
选择排序 | 时间复杂度O(n^2),空间复杂度O(1) 适用顺序结构、链式结构 | 稳定 |
堆排序 | 时间复杂度O(nlog2n),空间复杂度O(1) 适用于n较大的情况 只能使用顺序结构,不适用链式结构 | 不稳定 |
二路归并排序 | 时间复杂度O(nlog2n),空间复杂度O(n),需要借助工作栈 适用顺序结构、链式结构 | 稳定 |