1. 数组
- 数组的内存地址计算公式:f(index) = BaseAddress + DataTypeSize * Index
首地址 + 内存单个元素占用的内存大小 * 索引
搜索值
1 2 3 4 5 6 7 8
| int search(int arr[],int length,int key){ for(int i = 0;i < length;i++){ if(arr[i] == key){ return i; } } return -1; }
|
- 时间复杂度:
- 最好:首位,O(1)
- 末尾/不在数组中O(n)
插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdio.h> #define Max_SIZE 100
int main(){ int arr[Max_SIZE] = {1,5,10,7,31}; int length = 5; int element = 52; int pos = 2;
for(int i = length;i > pos; i--){ arr[i] = arr[i-1]; }
arr[pos] = element; length = length + 1; }
|
- 时间复杂度:
- 最好:末位,O(1)
- 最坏:首位O(n)
- 平均:O(n)
删除
1 2 3 4 5 6 7 8 9 10 11
| int rm(int arr[], int length, int key){ int index = search(arr,length,key); if(index == -1) return -1;
for(int i = index;i < length;i++){ aerr[i] = arr[i+1]; }
length = length-1; return index; }
|
- 时间复杂度:
- 最好:末位O(1)
- 最坏:首位O(n)
单链表
1 2 3 4
| struct LNode{ int data; struct LNode *next; };
|
头插法
1 2 3 4 5 6 7 8
| void insert(int e){ struct LNode *newNode = (LNode *)malloc(sizeof(struct LNode));
newNode->data = e; newNode->next = head; head = newNode; }
|
时间复杂度:O(1)
尾插法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void insertL(int e){ struct LNode *newNode = (LNode *)malloc(sizeof(struct LNode));
newNode->data = e; newNode->next = NULL;
if(head == NULL){ head = newNode; }else{ struct LNode *last = head; while(last->next != NULL){ last = last->next; } last->next = newNode; } }
|
时间复杂度:最好O(1) 最坏O(n)
删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| bool deleteN(struct LNode **head, int key){ struct LNode *temp;
if((*head)->data == key){ temp = *head; *head = (*head)->next; free(temp); return true; }else{ struct LNode *pre = *head; while(pre->next != NULL){ if(pre->next->data == key){ temp = pre->next; pre->next = pre->next->next; free(temp); return true; }else{ pre = pre->next; } }return false; } }
|
时间复杂度:最好O(1) 最坏O(n) 主要是遍历查找
搜索
1 2 3 4 5 6 7 8
| struct LNode *searchN(struct LNode *head,int key){ struct LNode *temp = head; while(temp != NULL){ if(temp->data == key) return temp; temp = temp->next; } return NULL; }
|
时间复杂度:最好O(1) 最坏O(n)
带头单链表
1 2 3 4
| typedef struct LNode{ int data; struct LNode *next; }LNode;
|
头节点一般不被当作链表元素
头节点的数据域不存储链表的数据
不用判断头指针是否为空,便于头指针和非空指针的统一处理
其它方法实现参照前文
双链表
1 2 3 4 5
| typedef struct DNode{ DNode *prior; DNode *next; int data; }DNode;
|
头插法
1 2 3 4 5 6 7 8 9 10 11 12
| bool insertH(DNode *head, int e){ DNode *newNode = (DNode*)malloc(sizeof(DNode)); newNode->data = e;
newNode->next = head->next; newNode->prior = head; if(head->next != NULL){ head->next->prior = newNode; } head->next = newNode; return true; }
|
时间复杂度O(1)
尾插法
1 2 3 4 5 6 7 8 9 10 11 12
| void insertL(DNode *head, int e){ DNode *newNode = (DNode*)malloc(sizeof(DNode)); newNode->data = e;
DNode *last = head; while(last->next != NULL){ last=last->next; } last->next = newNode; newNode->prior = last; newNode->next = NULL; }
|
时间复杂度O(n)
栈
- 先进后出
- 插入和删除只能在栈顶操作
- 入栈和出栈:O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #define size 3 int stack[size]; int top = -1;
void push(int e){ if(top == size-1){ cout << "Failed...Aready FULL!" << endl; return; } top = top + 1; stack[top] = e; }
void pop(){ if(top == -1){ cout << "Failed...Already EMPTY!" << endl; return; } top = top - 1; }
|
队列
- 先进先出
- 时间复杂度O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #define size 3 int quene[size]; int front = 0; int rear = 0;
void in(int val){ if(rear == size){ cout << "Aready FULL!" << endl; }else{ quene[rear] = val; rear++; } }
void qDelete(){ if(front == rear){ cout << "Already EMPTY!" << endl; }else{ front++; } }
|
树
- 每个结点可以有0/多个子结点
- 叶子结点:无子结点
- 一个节点的子结点个数:该节点的度
- 树中所有结点中度数最大的度:该树的度
- 层次:结点在树中的相对位置
- 第一层:根结点
- 第二层:根结点的子结点
······
- 双亲在同一层:堂兄弟结点
- 深度:从树的根结点开始,达到指定结点的层数
- 高度:从该结点出发,到离它最远的叶子结点的层数
- 树的高度和深度:树最大的层数
二叉树
- 每个结点最多有两个子结点:左子结点、右子结点
- 形态:
- 单结点树:只包含一个结点(只有一个根结点)
- 空二叉树:没有任何结点(根结点为空)
- 左斜树:只有左子结点,没有右子结点
- 右斜树:只有右子结点,没有左子结点
- 满二叉树:叶子节点无左右子结点,其余节点均有左右子结点
- 完全二叉树:除最后一层叶子结点可以为空,其余结点必须填满,叶子结点尽可能靠左排列(叶子结点允许缺失)
- 存入数组之后的下标:
- i > 1时,结点i的双亲下标为i/2,向下取整
- 2*i <= n时,结点i的左孩子下标为2i,否则无左孩子
- 2*i + 1 <= n时,结点i的右孩子下标为2i+1,否则无右孩子
1 2 3 4 5 6 7 8
| A / \ B C / \ / \ D E F G
A B C D E F G 0 1 2 3 4 5 6 7
|
- 使用数组存储树:
- 结点的位置与数组的索引之间有明确的映射关系(完全二叉树的结构设计)
- 不是完全二叉树/满二叉树:需用空位表示树中缺失的结点
- 二叉树一般采用链式存储
链式存储
- 二叉树结点数量为n,则指针域数量为2*n,非空指针域数量n-1(根结点不被指针指向),空指针域数量n+1
1 2 3 4
| typedef struct treeNode{ int data; struct treeNode *lchild,*rchild; }BNode;
|
递归
太抽象了此处就放个示例
1 2 3 4 5 6
| int f(int n){ if(n == 1) return 1; if(n == 2) return 2;
return f(n-1) + f(n-2); }
|
遍历
1 2 3 4 5
| A / \ B C / \ / \ D E F G
|
- 先序
顺序:根,左,右
A B D E C F G
- 中序
顺序:左,根,右
D B E A F C G
- 后序
顺序:左,右,根
D E B F G C A
- 层次
递归遍历的代码(以中序为例)
1 2 3 4 5 6 7
| void inOrder(BNode *root){ if(root == NULL) return;
inOrder(root->lchild); cout << root->data << endl; inOrder(root->rchild); }
|
哈夫曼树
- 哈夫曼编码条件:
- 每单个字符都必须映射到一个唯一的二进制编码
- 保证解码器能进行唯一解码(无损压缩)
- 保证任何一个编码都不能是其他编码的前缀
- 无损压缩的选择:
- 使用尽量少的bit数进行压缩
- 一个字符在原文中出现概率越大,则分配更短的二进制进行表示
- 香农熵(自己查罢)
1 2 3 4 5 6 7 8 9
| (香农-范诺编码示意图:1/4等概率) 0 1 --------A or B-------- | | 0 | 1 0 | 1 ---is A ?--- ---is B ?--- | | | | A B C D 00 01 10 11 (压缩编码方案)
|
哈夫曼编码添加了贪心算法
图
- 图是由一组顶点集合和边集合组成,每条边连接两个顶点
- 顶点集V={A,B,C,D},边集E={(A,B),(A,C),(A,D),(B,D)},G=(V,E)
- V不能为空集,E可以为空
- 无向图:无向边E={(,),(,)}
- (a,b) = (b,a)
- 有向图:有向边E={<,>,<,>}
- <a,b> != <b,a>
- 顶点的度:依附于该顶点边的条数TD
- 有向图:入度和出度
- 入度:以该顶点为终点的有向边的数量ID
- 初度:以该顶点为起点的有向边的数量OD
- TD = ID + OD
- 路径:从一个顶点到另一个顶点的顶点序列
- 无向图对路径没有要求
- 有向图:路径的方向必须和弧的方向一致
- 回路(环):一条路径的出发顶点和结束顶点为同一顶点
- 连通:
- 无向图:一个顶点到另一个顶点之间有路径存在
- 有向图:强连通:两个顶点,有一个正向的路径可以到达,同时也有相反方向的路径到达
- 连通图:
- 无向图:图中任意两个顶点都是连通的
- 有向图:图中任意两个顶点都是强连通的(强连通图)
- 有n个顶点的无向图,至少要n-1条边才是连通图
- 有n个顶点的有向图,至少要n条边才是强连通图
- 极大连通子图:包含尽可能多的顶点和边
- 连通分量:无向图中的极大连通子图
- 强连通分量:有向图中的极大强连通子图
- 带权图:边上含有权值
邻接矩阵
- 无向图(不带权):
$$
A_{ij} = \begin{cases}
1, & \text{当 $i$ 和 $j$ 之间存在边} \
0, & \text{当 $i$ 和 $j$ 之间不存在边}
\end{cases}
$$ 1 2 3 4 5 6 7 8 9 10
| A B C D ----------------- A| | | | | ----------------- B| | | | | ----------------- C| | | | | ----------------- D| | | | | -----------------
|
- 上三角和下三角区域相等,即关于主轴对称
- 主轴位置为自己到自己的边
- 有向图(不带权):
$$
A_{ij} = \begin{cases}
1, & \text{当 $i$ 和 $j$ 之间存在弧} \
0, & \text{当 $i$ 和 $j$ 之间不存在弧}
\end{cases}
$$
- 带权图:在边或弧对应的位置存储两个顶点之间的权值,若不存在边,可以设为无穷或其他特殊符号,自己指向自己的权值为0
- 相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #define MAX_VEX 6 typedef struct{ char vexs[MAX_VEX]; int arc[MAX_VEX][MAX_VEX]; }MGraph;
void GraphInit(MGraph *G){ for(int i = 0;i < MAX_VEX;i++){ for(int j = 0;j < MAX_VEX;j++){ G->arc[i][j] = 0; } } }
G.arc[0][1] = 1;
int GraphD(MGraph G, int vexIndex){ int d = 0; for(int i = 0;i < MAX_VEX;i++){ if(G.arc[vexIndex][i] != 0) d++; } return d; }
int Graph_D(MGraph G, int vexIndex){ int id = 0; int od = 0; for(int i = 0;i < MAX_VEX;i++){ if(G.arc[i][vexIndex] != 0) id++; } for(int i = 0;i < MAX_VEX;i++){ if(G.arc[vexIndex][i] != 0) od++; } return id + od; }
|
邻接链表
1 2 3 4 5 6 7
| // 示例为有向图 A->B,A->C 链表: A(src==0) 2(C)->1(B)->NULL 其中2 1为dest 以此类推 用一个数组存储链表,A:adjList[0]....以此类推
|
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| #define V 5
typedef struct VNode{ int data; VNode *next; }VNode;
VNode *adjList[V];
void init(){ for(int i = 0;i < V;i++){ adjList[i] = NULL; } }
void addEdge(int src, int dest){ VNode* newNode = (VNode*)malloc(sizeof(VNode)); newNode->data = dest;
newNode->next = adjList[src]; adjList[src] = newNode; }
void add_edge(int src, int dest){ VNode* newNode = (VNode*)malloc(sizeof(VNode)); newNode->data = dest;
newNode->next = adjList[src]; adjList[src] = newNode;
VNode* newNode1 = (VNode*)malloc(sizeof(VNode)); newNode1->data = src; newNode1->next = adjList[dest]; adjList[dest] = newNode1; }
int GraphD(int src){ int d = 0; VNode *node = adjList[src]; while(node != NULL){ d++; node = node->next; } return d; }
int Graph_D(int src){ int od = 0; VNode *node = adjList[src]; while(node != NULL){ od++; node = node->next; }
int id = 0; for(int i = 0;i < V;i++){ VNode *node1 = adjList[i]; while(node1 != NULL){ if(node1->data == src){ id++; } node1 = node1->next; } } return id+od; }
|
邻接链表和邻接矩阵对比
- 稠密图:边的数量接近最大边数,或约等于$V^2$,每个顶点都相互连接
- 稀疏图:边的数量接近最小边数,或等于V
- 邻接链表存储稀疏图
使用邻接矩阵会浪费大量内存空间(空间复杂度高)
- 邻接矩阵存储稠密图
最大化利用存储空间来存储边集
- 检查一条边是否存在:
- 邻接矩阵:检查对应的元素位置是否为1 O(1)
- 邻接链表:遍历链表,找到指定的节点是否存在 O(V)
- 创建边:
- 邻接矩阵:设置arc[i][j] = 1 O(1)
- 邻接链表:头插法 O(1)
- 删除边:
- 邻接矩阵:设置arc[i][j] = 0 O(1)
- 邻接链表:遍历链表,找到待删除的节点并删除 O(V)
- 新增顶点:
- 邻接矩阵:新建矩阵,复制原矩阵内容 O(V)
- 邻接链表:在顶点表中添加一个元素 O(1)
广度优先搜索算法(BFS)
- 访问目标顶点,再访问其所有距离为1的顶点(邻居顶点),然后访问距离为2的顶点,以此类推
- 权值相等时,BFS是解决最短路径问题的首选方法
- 相同距离的邻接顶点的访问顺序并不重要->BFS序列的顺序不唯一
- 代码实现:
- 从初始节点开始,逐层访问所有相邻顶点(先进先出->队列)
- 顶点被访问,其邻居被加入队列的末尾,当前顶点从队列的前端被取出
- 搜索相邻顶点:
- 邻接矩阵:找对应的出边
- 邻接链表:找对应的链表,遍历