1 链表

链表可以存储多个值,它的特点是将数据与指标配对,以及指向下一条数据的内存位置。

在链表中,数据存储在内存中分散的位置,所以每个数据只能通过前面的指标访问。

添加数据只需要将上一个数据的指向新数据,新数据再指向下一个。

链表分为单向链表、双向链表、环形链表和静态链表。

静态链表就是用连续内存保存链表数据,同时有指向下一个地址的参数。

使用场景:表长难以预估,经常要增加删除元素

  1. package main
  2. import "fmt"
  3. /**
  4. 链表的实现
  5. */
  6. type Node struct {
  7. data int
  8. next *Node
  9. }
  10. func main() {
  11. n3 := &Node{data: 3, next: nil}
  12. n2 := &Node{data: 2, next: n3}
  13. n1 := &Node{data: 1, next: n2}
  14. head := &Node{data: 0, next: n1}
  15. walkNode(head)
  16. }
  17. func walkNode(node *Node) {
  18. fmt.Println("node.data=", node.data)
  19. if node.next == nil {
  20. return
  21. }
  22. walkNode(node.next)
  23. }

1.2 线性表

零个或多个数据元素的有限序列。
它是序列,一个连着一个。
有两种实现方式:数组和链表。
操作:增删改查。
根据元素查索引,根据索引查元素。

  1. package main
  2. import "fmt"
  3. /**
  4. 顺序表
  5. 用数组实现
  6. */
  7. const initLen = 10 // 默认长度为10
  8. type SqList struct {
  9. data [initLen]int // 用数组保存
  10. Length int // 存值的长度
  11. }
  12. func NewSqList() *SqList {
  13. return &SqList{}
  14. }
  15. // AddElement 添加元素
  16. func (s *SqList) AddElement(e int) int {
  17. // 判断数组越界
  18. if s.Length >= len(s.data) {
  19. panic("数组满了")
  20. }
  21. s.data[s.Length] = e
  22. s.Length++
  23. return 0
  24. }
  25. // 重写打印方法
  26. func (s SqList) String() string {
  27. var d []int
  28. for i := 0; i < s.Length; i++ {
  29. d = append(d, s.data[i])
  30. }
  31. return fmt.Sprintf("data->%v, length->%d", d, s.Length)
  32. }

2 数组

数组可以存储多个值,通过索引访问。

数组中的数据存储在连续位置的存储器中。

添加元素时,要确保数组末尾有额外空间,然后一个一个移动数据,腾出要添加的位置;删除元素则相反,先删除元素,移动数据填补空白区域,最后删除额外的空间。

总结:

数组查找很快,通过索引直接找到,但是增加和删除很慢,需要移动其他数据位置。

表长可预估,查询操作较多。

3 栈

就想往一个盒子里放CD,先进后出。

可以用数组实现,也可以用链表实现。

栈用一个辅助指针。

4 队列

类似排队,先进先出。

栈和队列的本质也是数组,不过增加了约束条件。

队列用两个辅助指针。一个指向头,一个指向尾。

判断空和满,可以使用辅助变量或者。

双端队列,可以从两端插入和删除。

5 哈希表

以key和value组成键值对,然后准备一个数组,将键值对的哈希值取数组长度的余,放入数组中,如果有相同的余数,则放入列表里,这样可以快速访问数据,不过要谨慎使用。

6 堆

堆是一种树形结构,并在实现 优先队列 时使用。

优先队列可以按任意顺序添加数据,取出时选择最小值。

过程:

  1. 添加的数字放在末尾;
  2. 如果父类数字比较大,则子类与父类互换;
  3. 以此类推,直到不发生替换;
  4. 取出数字时,从最上面的数字取出;
  5. 将结尾的数字移动到顶部;
  6. 重新开始比较子类与父类,直到不发生替换为止

使用堆可以快速取出最小的数据,但是无法执行在树中间取出数据的操作。

7 二叉树查找

二叉树有两个属性:第一个是所有节点都比左子树大,第二个是所有节点都比右子树小。

添加节点:从顶端节点开始,逐个比较,放到合适位置。

删除节点:

  1. 如果没有子节点直接删除;
  2. 如果有一个子节点,移动子节点到删除的位置;
  3. 有两个子节点,可以从左子树中找最大的节点移动到删除的位置,也可以找右子树最小值。

一直保持良好平衡的二叉树称为 自平衡二叉树 ,能保持搜索效率。

8 集合

一种不允许重复元素的数据结构。

这也是和数组最大的区别。所以它的插入比一般的数组慢。

9 串

即字符串,由零个或者多个字符组成的有限序列。

字符串的比较:

从第一个字符开始往后依次对比其ASCII码。

中文根据Unicode比较大小。

举例:

  1. char a[] = "哎bc";
  2. char b[] = "呀bc";
  3. int c = strcmp(a, b);
  4. printf("compare a and b:%d\n", c);

匹配字符子串的算法:KPM算法。

10 树

树是n个结点的有限集。n>=0。

树的遍历:前序遍历、中序遍历、后序遍历和层序遍历。

  • 前序遍历:根->左->右
  • 中序遍历:左->根->右
  • 后序遍历:左->右-根

根据根节点位置来区分。

完全二叉树

每层都满足 2^n-1 函数。

可以用顺序存储结构(一维数组)或者链式存储结构。

一般用后者,前者浪费空间。

二叉树的表示

  1. typedef char ElemType;
  2. typedef struct BitNode
  3. {
  4. ElemType data;
  5. struct BitNode *lchild, *rchild;
  6. } BitNode, *BiTree;

创建二叉树用前序遍历。

遍历二叉树

  1. // 二叉树遍历
  2. #include <stdio.h>
  3. typedef char ElemType;
  4. typedef struct BitNode
  5. {
  6. ElemType data;
  7. struct BitNode *lchild, *rchild;
  8. } BitNode, *BiTree;
  9. // 前序:A,B,C
  10. // 中序:B,A,C
  11. // 后序:B,C,A
  12. void bianli(BitNode *tree)
  13. {
  14. if (tree->lchild != NULL)
  15. {
  16. bianli(tree->lchild);
  17. }
  18. if (tree->rchild != NULL)
  19. {
  20. bianli(tree->rchild);
  21. }
  22. // 根节点位置
  23. if (tree != NULL)
  24. {
  25. printf("data=%c\n", tree->data);
  26. }
  27. }
  28. int main(int argc, char const *argv[])
  29. {
  30. BitNode right1 = {'C', NULL, NULL};
  31. BitNode left1 = {'B', NULL, NULL};
  32. BitNode root = {'A', &left1, &right1};
  33. bianli(&root);
  34. return 0;
  35. }

线索二叉树

  • 若无左子树,则将左指针指向其前驱节点;
  • 若无右子树,则将右指针指向其后继节点;
  1. // 线索二叉树
  2. #include <stdio.h>
  3. typedef struct ThreadNode {
  4. int data;
  5. struct ThreadNode *lchild, *rchild; // 左右子树
  6. int ltag, rtag; // 左右标志位,0表示指向子节点,1表示指向前后继节点
  7. } ThreadNode, *ThreadTree;
  8. void InThread(ThreadTree &p, ThreadNode &pre) {
  9. if (p!=NULL) {
  10. InThread(p->lchild, pre);
  11. }
  12. if (p->lchild==NULL) {
  13. p->lchild = &pre;
  14. p->ltag = 1;
  15. }
  16. if (&pre!=NULL && pre.rchild != NULL) {
  17. pre.rchild = p;
  18. pre.rtag = 1;
  19. }
  20. pre = *p;
  21. InThread(p->rchild, pre);
  22. }

哈夫曼树

当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。

在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。

利用哈夫曼树构造的用于通信的二进制编码称为哈夫曼编码。哈夫曼编码可以有效的压缩数据。

11.图

多对多逻辑关系数据的结构。

图的存储结构:

邻接矩阵

用一个一维数组存储图的顶点,一个二维数组(即邻接矩阵)存储图中的边或者弧的信息。

不过比较浪费空间。

通常,图更多的是采用链表存储,具体的存储方法有 3 种,分别是邻接表、邻接多重表和十字链表。

邻接表

邻接表存储图的实现方式是,给图中的各个顶点独自建立一个链表,用节点存储该顶点,用链表中其他节点存储各自的临界点。

但是非常不方便同时计算出度和入度。

十字链表

十字链表实质上就是为每个顶点建立两个链表,分别存储以该顶点为弧头的所有顶点和以该顶点为弧尾的所有顶点。

  • tailvex:用于存储以首元节点为弧尾的顶点位于数组中的位置下标;
  • headvex:用于存储以首元节点为弧头的顶点位于数组中的位置下标;
  • hlink 指针:用于链接下一个存储以首元节点为弧头的顶点的节点;
  • tlink 指针:用于链接下一个存储以首元节点为弧尾的顶点的节点;
  • info 指针:用于存储与该顶点相关的信息,例如量顶点之间的权值;

代码

  1. #define MAX_VERTEX_NUM 20
  2. #define InfoType int//图中弧包含信息的数据类型
  3. #define VertexType int
  4. typedef struct ArcBox{
  5. int tailvex,headvex;//弧尾、弧头对应顶点在数组中的位置下标
  6. struct ArcBox *hlik,*tlink;//分别指向弧头相同和弧尾相同的下一个弧
  7. InfoType *info;//存储弧相关信息的指针
  8. }ArcBox;
  9. typedef struct VexNode{
  10. VertexType data;//顶点的数据域
  11. ArcBox *firstin,*firstout;//指向以该顶点为弧头和弧尾的链表首个结点
  12. }VexNode;
  13. typedef struct {
  14. VexNode xlist[MAX_VERTEX_NUM];//存储顶点的一维数组
  15. int vexnum,arcnum;//记录图的顶点数和弧数
  16. }OLGraph;
  17. int LocateVex(OLGraph * G,VertexType v){
  18. int i=0;
  19. //遍历一维数组,找到变量v
  20. for (; i<G->vexnum; i++) {
  21. if (G->xlist[i].data==v) {
  22. break;
  23. }
  24. }
  25. //如果找不到,输出提示语句,返回 -1
  26. if (i>G->vexnum) {
  27. printf("no such vertex.\n");
  28. return -1;
  29. }
  30. return i;
  31. }
  32. //构建十字链表函数
  33. void CreateDG(OLGraph *G){
  34. //输入有向图的顶点数和弧数
  35. scanf("%d,%d",&(G->vexnum),&(G->arcnum));
  36. //使用一维数组存储顶点数据,初始化指针域为NULL
  37. for (int i=0; i<G->vexnum; i++) {
  38. scanf("%d",&(G->xlist[i].data));
  39. G->xlist[i].firstin=NULL;
  40. G->xlist[i].firstout=NULL;
  41. }
  42. //构建十字链表
  43. for (int k=0;k<G->arcnum; k++) {
  44. int v1,v2;
  45. scanf("%d,%d",&v1,&v2);
  46. //确定v1、v2在数组中的位置下标
  47. int i=LocateVex(G, v1);
  48. int j=LocateVex(G, v2);
  49. //建立弧的结点
  50. ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));
  51. p->tailvex=i;
  52. p->headvex=j;
  53. //采用头插法插入新的p结点
  54. p->hlik=G->xlist[j].firstin;
  55. p->tlink=G->xlist[i].firstout;
  56. G->xlist[j].firstin=G->xlist[i].firstout=p;
  57. }
  58. }

邻接多重表

邻接多重表存储无向图的方式,可看作是邻接表和十字链表的结合。同邻接表和十字链表存储图的方法相同,都是独自为图中各顶点建立一张链表,存储各顶点的节点作为各链表的首元节点,同时为了便于管理将各个首元节点存储到一个数组中。

注意,邻接多重表仅适用于存储无向图或无向网。