- 线性结构
- 非线性结构
- 树
- 图
- 集合
- 顺序存储
- 链接存储
- 索引存储
- 在存储的同时,还建立附加的索引表
- 散列存储
线性表的顺序存储结构是一种随机存取的存储结构,线性表的链式存储结构是一种顺序存取的存储结构.
// 初始化
Q.rear=Q.front=0;
// 判断空
Q.rear==Q.front;
// 判断队满
(Q.rear+1)%MAXSIZE==Q.front;
// 队列长度
(Q.rear+MAXSIZE-Q.front)%MAXSIZE;
对于矩阵中实际存储的元素<<矩阵大小时,可以只存取三元组(行标,列标,值)的list.
-
度
树中一个节点的子节点个数称为该节点的度,树中最大度数称为树的度
-
深度
从根节点开始自顶向下逐层累加
-
高度
从叶节点开始自底向上逐层累加
-
树的高度(深度)
树中节点最大的层数
N0表示叶子节点数,N2表示度为2的节点则N0=N2+1
故在有N个节点的二叉树中,有N+1个空指针;空指针数 = 2*N0+N1=N0+N1+N2+1
If a complete binary tree with n nodes is represented sequentially, then for any node with index i,
- parent(i) is at int(i/2) if
$i \neq 1$ . If i = 1, i is at the root and has no parent. - LeftChild(i) is at 2i if
$2i \leq n$ . If$2i \geq n$ , then i has no left child. - RightChild(i) is at 2i+1 if
$2i+1 \leq n$ . If$2i+1 \geq n$ , then i has no right child.
- 度为2的树至少含有三个节点才可以,二叉树则可为空
- 度为2的树如果某个节点只有一个孩子节点则无需分左右,而二叉树不同
-
满二叉树
叶子节点都在二叉树的最下一层,并且除叶节点之外的每个节点的度数都为2
-
完全二叉树
每个节点都与高度一样的满二叉树编号一一对应.也就是只有最下一层的右边可以缺少元素.
-
平衡二叉树
树上任意节点的左子树和右子树的深度之差不超过1
-
先序遍历(PreOrder)
顺序:根节点,先序遍历左子树,先序遍历右子树
实现: 递归实现
非递归实现
void PreOrder2(BiTree t){ InitStack(S); BiTree p = t; while(p||!IsEmpty(S)){ if(p){ visit(p); Push(S,p); p = p->lchild; } else{ Pop(S,p); p=p->rchild; } } }
-
中序遍历(InOrder)
顺序:中序遍历左子树,根节点,中序遍历右子树
实现: 递归实现
非递归实现
void InOrder2(BiTree t){ InitStack(S); BiTree p = t; while(p||!IsEmpty(S)){ if(p){ Push(S,p); p = p->lchild; } else{ Pop(S,p); visit(p); p=p->rchild; } } }
-
后序遍历(PostOrder)
顺序:后序遍历左子树,后序遍历右子树,根节点
实现: 递归实现
非递归实现:
void PostOrder(BiTree T){ InitStack(S); BiTree p = T; BiTree r = NULL: while(p||!IsEmpty(S)){ if(p){ push(S,p); p = p->lchild; } else{ GetTop(S,p); if(p->rchild&&p->rchild!=r){ p=p->rchild; push(S,p); p=p->lchild; } else{ pop(S,p); visit(p->data); r=p; p=NULL; } } } }
根据先序遍历和中序遍历或后序遍历和中序遍历可以确定一颗二叉树,而先序和后序则不能确定一颗二叉树
void LevelOrder(BiTree T){
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);
}
}
}
int BitDepth(BiTree T){
if (T==NULL)
return 0;
else{
ldep = Btdepth(T->lchild);
rdep = Btdepth(T->rchild);
if(ldep>rdep)
return ldep+1;
else
return rdep+1;
}
}
格式:
ltag | lchild | data | rchild | rtag |
---|---|---|---|---|
1:指向节点的前驱 | 1:指向节点的后继 |
中序线索二叉树示例
用孩子兄弟表示法将树=>二叉树(由于根节点没有兄弟所以没有右子树)=>将森林的多颗树连在一起
例:
树和森林遍历与二叉树遍历的对应关系
树 | 森林 | 二叉树(将森林或树转换成的二叉树) |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
13+9=22
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。
treenode *huffmanTree(int n) { //n为叶子个数
int index = 0;
for (int i = 0; i < n; ++i) { //将每一个叶子节点建成一棵树,放入数组tree[]中
treenode *p = new treenode;
p->data = arr[i]; //arr[]中放着叶子节点的权值
p->lch = NULL;
p->rch = NULL;
tree[index++] = p;
}
treenode *root = NULL; //定义最优二叉树的根
for (int i = 0; i < n - 1; ++i) { //每次取arr[]中未使用的最小两个值
sort(arr + i, arr + n);
treenode *ch1 = NULL, *ch2 = NULL;
for (int j = 0; j < index; ++j) {//在数组tree[]中找到这两个值,建立新的节点
if(tree[j]->data == arr[i])
ch1 = tree[j];
if(tree[j]->data == arr[i + 1])
ch2 = tree[j];
if(ch1 && ch2) break;
}
treenode *p = new treenode;//建立新的节点,存入数组tree[]中
p->data = ch1->data + ch2->data;
p->lch = ch1;
p->rch = ch2;
root = p;//更新根,直到建立完成
tree[index++] = p;
arr[i + 1] = p->data;//并将这个节点的权值传入数组arr[]中
}
return root;
}
-
有向图
表示法:
V(G3)={0,1,2}
E(G3)={<0,1>,<1,0>,<1,2>}
强连通: 节点v到w和w到v都有路径
强连通分量: 极大强连通子图
入度,出度: 入度和=出度和=边数
-
无向图
表示法
V(G1)={0,1,2,3}
E(G1)={(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)}
连通: 从v到w的路径存在,则v和w是连通的.
连通图: 图G的任意两个顶点都是连通的.
连通分量: 极大连通子图; 连通图只有一个连通分量; 非连通图有多个连通分量
极大连通子图: 要求连通子图包含其所有的边
极小连通子图: 保持连通又使得边数最少(生成树)
-
完全图
任意两个顶点都存在边.
- n个顶点的有向图=>n(n-1)条边
- n个顶点的无向图=>n(n-1)/2条边
-
稀疏图|E|<|V|*log|V|和稠密图
-
邻接矩阵法
-
邻接表法
定义:
#define MaxVerNum 100 /*最大顶点数为100*/ // 邻接表类型 : typedef struct ArcNode { int adjvex; /*邻接点域*/ InfoType *Info; /*表示边上信息的域info*/ struct ArcNode * next; /*指向下一个邻接点的指针域*/ } ArcNode ; // 表头结点类型 : typedef struct Vnode { VertexType vertex; /*顶点域*/ ArcNode * firstedge; /*边表头指针*/ } Vnode, AdjList [MaxVertexNum]; // 图的类型 : typedef struct { AdjList vertices; /*邻接表*/ int vexnum, arcnum; /*顶点数和边数*/ } ALGraph; /*ALGraph是以邻接表方式存储的图类型*/
两个基本函数
int FirstAdjVex(ALGraph G, int v)
{
// 返回G中第v个顶点的第1个邻接点的序号。如果v无邻接点,返回0。
if(!G.vertices[v].firstarc)
return(0);
else
return(G.vertices[v].firstarc->adjvex);
}
int NextAdjVex(ALGraph G, int v, int w)
{
// 返回G中第v个顶点的相对于顶点w的下一个邻接点的序号。
// 如果v无相对于顶点w的下一个邻接点,返回0。
ArcNode *p;
p=G.vetrices[v].firstarc;
while( p && p->adjvex!=w)
p=p->nextarc;
if(p->adjvex==w && p->nextarc)
return(p->nextarc->adjvex);
else
return(0);
}
-
DFS(深度优先)
void DFSTraverse( Graph G ) { bool visited[MAX] ; //用于标识结点是否已被访问过 for ( k=1; k<=G.vexnum; ++k ) visited[k] = FALSE; for ( k=1; k<=G.vexnum; ++k ) if ( !visited[ k ] ) DFS(G, k); } void DFS( Graph G, int v ) { visited[v] = TRUE; VISIT( v ); // 访问图G中第v个顶点 for (w=FirstAdjVex(G, v); w>0; w=NextAdjVex(G, v, w)) if ( !visited[ w ] ) DFS(G, w); }
-
BFS(广度优先)
void BFSTraverse( Graph G ) { bool visited[MAX] ; //用于标识结点是否已被访问过 for (v=1; v<=G.vexnum; ++v) visited[v] = FALSE; InitQueue(Q); for(v=1; v<=G.vexnum; ++v) if ( !visited[v] ) { visited[v]=TRUE; VISIT(v); EnQueue(Q, v); while (!EmptyQueue(Q)) { DeQueue(Q,u); for(w=FirstAdjVex(G, u); w>0; w=NextAdjVex(G, u, w)) if( !visited[w] ) { visited[w]=TRUE; VISIT(w); EnQueue(Q, w); } } // end while } // end if }
-
AOV-Network (Activity on Vertex)
顶点表示活动,边<v1,v2>表示活动v1先于活动v2,必须是有向无环图.(依赖关系)
拓扑排序:
1. 从图中选择一个没有前驱的节点并输出 2. 从图中删除该节点和以它为起点的有向边 3. 重复步骤1,2
-
AOE-Network (Activity on Edge)
顶点表示事件,有向边表示活动,边上的权重表示完成该活动的开销.(软件项目安排)
关键路径: 从起点到终点的所有路径中,具有最大的路径长度的路径
1. 求AOE中所有事件的最早发生时间ve():从前往后计算 2. 求AOE中所有事件的最晚发生时间vl():从后往前计算 3. 求AOE中所有活动的最早开始时间e():边的起点的顶点的最早发生时间 4. 求AOE中所有活动的最晚开始时间l():边的终点的顶点的最晚发生时间-边的权重 5. 求AOE中所有活动的差额d(),找出d()=0构成关键路径
-
插入排序
// 直接插入排序 void InsertSort(int a[], int length){ /* * 插入排序 * 从头开始保证走过的路有序,每加入一个元素就将其插入到正确的位置 */ int j,i; for(i=2;i<length;i++){ if (a[i]<a[i-1]){ a[0] = a[i]; for(j=i-1;a[0]<a[j]; j--) a[j+1] = a[j]; a[j+1] = a[0]; } } } // 折半插入排序 void InsertSort(ElemType A[], int n){ int i,j,low,high,mid; for(i=2;i<=n;i++){ A[0]=A[i]; low=1; high=i-1; while(low<=high){ mid=(low+high)/2 if(A[mid].key>A[0].key) 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]; } } // 希尔排序 void ShellSort(SqList &L,int dlta[],int t) { // 按增量序列dlta[0..t-1]对顺序表L作希尔排序。算法.5 int k; for(k=0;k<t;++k){ ShellInsert(L,dlta[k]); // 一趟增量为dlta[k]的插入排序 printf("第%d趟排序结果: ",k+1); print(L); } } void ShellInsert(SqList &L,int dk){ // 对顺序表L作一趟希尔插入排//序。本算法是和一趟直接插入排序相比, // 作了以下修改: // 1.前后记录位置的增量是dk,而不是1; // 2.r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。 int i,j; for(i=dk+1;i<=L.length;++i){ if LT(L.r[i].key,L.r[i-dk].key){ // 需将L.r[i]插入有序增量子表 L.r[0]=L.r[i]; // 暂存在L.r[0] for(j=i-dk;j>0&<(L.r[0].key,L.r[j].key);j-=dk) L.r[j+dk]=L.r[j]; // 记录后移,查找插入位置 L.r[j+dk]=L.r[0]; // 插入 } } }
-
选择排序
void SelectSort(ElemType A[], int n){ for (i=0;i<n-1;i++){ int min = i; for(j=i+1; j<n; j++){ if(A[j]<A[min]) min = j; } if (min!=i) swap(A[i], A[min]); } }
-
冒泡排序
void bubble_sort(int a[],int n) { // 将a中整数序列重新排列成自小至大有序的整数序列(起泡排序) int i,j,t; Status change = TRUE; for(i=n-1;i>1&&change;--i){ change=FALSE; for(j=0;j<i;++j) if(a[j]>a[j+1]){ t=a[j]; a[j]=a[j+1]; a[j+1]=t; change=TRUE; } } }
-
快速排序
void QuickSort(int a[], int low, int high){ if (low<high){ int mid = partition(a, low,high); QuickSort(a, low, mid-1); QuickSort(a, mid+1, high); } } int partition(int a[],int low,int high){ int pivot = a[low]; while(low<high){ while(a[high]>=pivot && low<high) high--; a[low] = a[high]; while(a[low]<=pivot && low<high) low++; a[high] = a[low]; } a[low] = pivot; return low; }
-
堆排序
算法步骤:
-
构造初始堆
- 假设给定无序序列结构如下
- 此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
- 找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
- 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6
-
调整堆
- 将堆顶元素9和末尾元素4进行交换
- 重新调整结构,使其继续满足堆定义
- 再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
- 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
void AdjustDown(int a[], int k, int len){ a[0] = a[k]; for (int i=k*2; i<=len; i*=2){ if(i<len&&a[i]<a[i+1]) i++; if(a[0]>=a[i]) break; else{ a[k] = a[i]; k=i; } } a[k] = a[0]; } void HeapSort(int a[], int length){ // build max heap for (int i=length/2; i>0; i--) AdjustDown(a, i, length); for (int i=length;i>1; i--){ int t=a[1]; a[1] = a[i]; a[i] = t; AdjustDown(a,1, i-1); } }
-
-
归并排序
void merge(int a[], int low, int mid, int high, int b[]){ for(int i=low; i<=high; i++) b[i] = a[i]; int l,h,k; for (k=low,l=low,h=mid+1; l<=mid&&h<=high;k++){ if(b[l]<=b[h]) a[k] = b[l++]; else a[k] = b[h++]; } while(l<=mid) a[k++]=b[l++]; while(h<=high) a[k++]=b[h++]; } void MergeSort(int a[], int low, int high, int b[]){ if (low<high){ int mid = (low+high)/2; MergeSort(a, low, mid,b); MergeSort(a,mid+1, high, b); merge(a, low, mid , high, b); } } void MergeSort(int a[], int len){ int *b = new int[len]; MergeSort(a, 1, len, b); delete []b; }
-
基数排序
将所有待比较数值(正整数)统一为同样的数字长度,数字较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
时间复杂度:O(d(n+r))
其中d:d趟分配与收集;一趟分配:O(n);一趟收集:O(r)
排序名称 | 类别 | 稳定性 | 时间复杂度 | 空间复杂度 | 最少比较次数 | 最多比较次数 | 最少移动次数 | 最多移动次数 |
---|---|---|---|---|---|---|---|---|
插入排序 | 插入排序 | 稳定 | O(n2) | O(1) | n-1 | (n+2)(n-1)/2 | 0 | (n+4)(n-1)/2 |
希尔排序 | 插入排序 | 不稳定 | O(n1.5) | o(1) | / | / | / | / |
冒泡排序 | 交换排序 | 稳定 | O(n2) | o(1) | n-1 | n(n-1)/2 | 0 | n(n-1)/2 |
快速排序 | 交换排序 | 不稳定 | O(n*log2n) | o(logn) | O(n*log2n) | n(n-1)/2 | O(n*log2n) | / |
选择排序 | 选择排序 | 不稳定 | O(n2) | O(1) | n-1 | n(n-1)/2 | 0 | 3(n-1) |
堆排序 | 选择排序 | 不稳定 | O(n*log2n) | O(1) | O(n*log2n) | O(n*log2n) | / | / |
归并排序 | 归并排序 | 稳定 | O(n*log2n) | O(n) | O(n*log2n) | O(n*log2n) | / | / |
基数排序 | 基数排序 | 稳定 | O(d(n+rd)) | O(rd) | 0 | 0 | 0 | 0 |
排序名称 | 平均情况 | 最好情况 | 最坏情况 |
---|---|---|---|
直接插入排序 | O(n2) | O(n) | o(n2) |
希尔排序 | O(n1.5) | ||
冒泡排序 | O(n2) | O(n) | O(n2) |
快速排序 | O(n*log2n) | O(n*log2n) | O(n2) |
简单选择排序 | O(n2) | O(n2) | O(n2) |
堆排序 | O(n*log2n) | O(n*log2n) | O(n*log2n) |
归并排序 | O(n*log2n) | O(n*log2n) | O(n*log2n) |
基数排序 | O(d(n+rd)) | O(d(n+rd)) | O(d(n+rd)) |
-
线性索引结构
-
静态搜索树
查找表中各关键字被查找的概率是不同的
含有 9 个关键字的查找表及其相应权值如下表所示:
-
求出查找表中所有的 △P 的值,找出整棵查找表的根结点:
例如,关键字 F 的 △P 的计算方式为:从 G 到 I 的权值和 - 从 A 到 E 的权值和 = 4+3+5-1-1-2-5-3 = 0。
-
通过上图左侧表格得知,根结点为 F,以 F 为分界线,左侧子表为 F 结点的左子树,右侧子表为 F 结点的右子树(如上图右侧所示),继续查找左右子树的根结点:
-
通过重新分别计算左右两查找子表的 △P 的值,得知左子树的根结点为 D,右子树的根结点为 H (如上图右侧所示),以两结点为分界线,继续判断两根结点的左右子树:
注意:在建立次优查找树的过程中,由于只根据的各关键字的 P 的值进行构建,没有考虑单个关键字的相应权值的大小,有时会出现根结点的权值比孩子结点的权值还小,此时就需要适当调整两者的位置。
-
m阶: 1<子节点数量<=M ,M>=2,空树除外(注:m阶代表一个树节点最多有多少个查找路径,m阶=m路,当m=2则是2叉树,m=3则是3叉);
-
Hash 函数
-
直接地址法
H(key)=a*key+b
适合于关键字分布基本连续
-
除留余数法
H(key)=key%p
假定散列表长为m,取一个不大于m但接近或等于m的质数P
-
数字分析法
适用于已知关键字数字集合
-
平方取中法
取关键字的平方值的中间几位作为散列地址
-
折叠法
将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址
-
-
冲突处理
-
开放地址法
递推式: Hi=(H(key) + di) % m
di是增量
删除时不能随便物理删除表中已有的元素,若删除将会截断其他具有相同的散列地址的元素的查找地址,所以要进行逻辑删除,打一个标记,定期维护,将删除标记的元素物理删除.
-
线性探索法
往下一个一个找
-
平方探索法
当di = 1^2,-1^2,2^2,-2^2,...,k^2,-k^2,其中$k \leq m/2$, m必须为可以表示成4k+3的质数
可以避免出现堆积的问题,但是不能探测到所有单元
-
再散列法
di=Hash2(key)
-
伪随机法
di=伪随机序列
-
-
拉链法
往后拉链表
-
-
装填因子
a = 表中记录数n/散列表长度m