1 链表
链表可以存储多个值,它的特点是将数据与指标配对,以及指向下一条数据的内存位置。
在链表中,数据存储在内存中分散的位置,所以每个数据只能通过前面的指标访问。
添加数据只需要将上一个数据的指向新数据,新数据再指向下一个。
链表分为单向链表、双向链表、环形链表和静态链表。
静态链表就是用连续内存保存链表数据,同时有指向下一个地址的参数。
使用场景:表长难以预估,经常要增加删除元素
package mainimport "fmt"/**链表的实现*/type Node struct {data intnext *Node}func main() {n3 := &Node{data: 3, next: nil}n2 := &Node{data: 2, next: n3}n1 := &Node{data: 1, next: n2}head := &Node{data: 0, next: n1}walkNode(head)}func walkNode(node *Node) {fmt.Println("node.data=", node.data)if node.next == nil {return}walkNode(node.next)}
1.2 线性表
零个或多个数据元素的有限序列。
它是序列,一个连着一个。
有两种实现方式:数组和链表。
操作:增删改查。
根据元素查索引,根据索引查元素。
package mainimport "fmt"/**顺序表用数组实现*/const initLen = 10 // 默认长度为10type SqList struct {data [initLen]int // 用数组保存Length int // 存值的长度}func NewSqList() *SqList {return &SqList{}}// AddElement 添加元素func (s *SqList) AddElement(e int) int {// 判断数组越界if s.Length >= len(s.data) {panic("数组满了")}s.data[s.Length] = es.Length++return 0}// 重写打印方法func (s SqList) String() string {var d []intfor i := 0; i < s.Length; i++ {d = append(d, s.data[i])}return fmt.Sprintf("data->%v, length->%d", d, s.Length)}
2 数组
数组可以存储多个值,通过索引访问。
数组中的数据存储在连续位置的存储器中。
添加元素时,要确保数组末尾有额外空间,然后一个一个移动数据,腾出要添加的位置;删除元素则相反,先删除元素,移动数据填补空白区域,最后删除额外的空间。
总结:
数组查找很快,通过索引直接找到,但是增加和删除很慢,需要移动其他数据位置。
表长可预估,查询操作较多。
3 栈
就想往一个盒子里放CD,先进后出。
可以用数组实现,也可以用链表实现。
栈用一个辅助指针。
4 队列
类似排队,先进先出。
栈和队列的本质也是数组,不过增加了约束条件。
队列用两个辅助指针。一个指向头,一个指向尾。
判断空和满,可以使用辅助变量或者。
双端队列,可以从两端插入和删除。
5 哈希表
以key和value组成键值对,然后准备一个数组,将键值对的哈希值取数组长度的余,放入数组中,如果有相同的余数,则放入列表里,这样可以快速访问数据,不过要谨慎使用。
6 堆
堆是一种树形结构,并在实现 优先队列 时使用。
优先队列可以按任意顺序添加数据,取出时选择最小值。
过程:
- 添加的数字放在末尾;
- 如果父类数字比较大,则子类与父类互换;
- 以此类推,直到不发生替换;
- 取出数字时,从最上面的数字取出;
- 将结尾的数字移动到顶部;
- 重新开始比较子类与父类,直到不发生替换为止
使用堆可以快速取出最小的数据,但是无法执行在树中间取出数据的操作。
7 二叉树查找
二叉树有两个属性:第一个是所有节点都比左子树大,第二个是所有节点都比右子树小。
添加节点:从顶端节点开始,逐个比较,放到合适位置。
删除节点:
- 如果没有子节点直接删除;
- 如果有一个子节点,移动子节点到删除的位置;
- 有两个子节点,可以从左子树中找最大的节点移动到删除的位置,也可以找右子树最小值。
一直保持良好平衡的二叉树称为 自平衡二叉树 ,能保持搜索效率。
8 集合
一种不允许重复元素的数据结构。
这也是和数组最大的区别。所以它的插入比一般的数组慢。
9 串
即字符串,由零个或者多个字符组成的有限序列。
字符串的比较:
从第一个字符开始往后依次对比其ASCII码。
中文根据Unicode比较大小。
举例:
char a[] = "哎bc";char b[] = "呀bc";int c = strcmp(a, b);printf("compare a and b:%d\n", c);
匹配字符子串的算法:KPM算法。
10 树
树是n个结点的有限集。n>=0。
树的遍历:前序遍历、中序遍历、后序遍历和层序遍历。
- 前序遍历:根->左->右
- 中序遍历:左->根->右
- 后序遍历:左->右-根
根据根节点位置来区分。
完全二叉树
每层都满足 2^n-1 函数。
可以用顺序存储结构(一维数组)或者链式存储结构。
一般用后者,前者浪费空间。
二叉树的表示
typedef char ElemType;typedef struct BitNode{ElemType data;struct BitNode *lchild, *rchild;} BitNode, *BiTree;
创建二叉树用前序遍历。
遍历二叉树
// 二叉树遍历#include <stdio.h>typedef char ElemType;typedef struct BitNode{ElemType data;struct BitNode *lchild, *rchild;} BitNode, *BiTree;// 前序:A,B,C// 中序:B,A,C// 后序:B,C,Avoid bianli(BitNode *tree){if (tree->lchild != NULL){bianli(tree->lchild);}if (tree->rchild != NULL){bianli(tree->rchild);}// 根节点位置if (tree != NULL){printf("data=%c\n", tree->data);}}int main(int argc, char const *argv[]){BitNode right1 = {'C', NULL, NULL};BitNode left1 = {'B', NULL, NULL};BitNode root = {'A', &left1, &right1};bianli(&root);return 0;}
线索二叉树
- 若无左子树,则将左指针指向其前驱节点;
- 若无右子树,则将右指针指向其后继节点;
// 线索二叉树#include <stdio.h>typedef struct ThreadNode {int data;struct ThreadNode *lchild, *rchild; // 左右子树int ltag, rtag; // 左右标志位,0表示指向子节点,1表示指向前后继节点} ThreadNode, *ThreadTree;void InThread(ThreadTree &p, ThreadNode &pre) {if (p!=NULL) {InThread(p->lchild, pre);}if (p->lchild==NULL) {p->lchild = ⪯p->ltag = 1;}if (&pre!=NULL && pre.rchild != NULL) {pre.rchild = p;pre.rtag = 1;}pre = *p;InThread(p->rchild, pre);}
哈夫曼树
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。
在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。
利用哈夫曼树构造的用于通信的二进制编码称为哈夫曼编码。哈夫曼编码可以有效的压缩数据。
11.图
多对多逻辑关系数据的结构。
图的存储结构:
邻接矩阵
用一个一维数组存储图的顶点,一个二维数组(即邻接矩阵)存储图中的边或者弧的信息。
不过比较浪费空间。
通常,图更多的是采用链表存储,具体的存储方法有 3 种,分别是邻接表、邻接多重表和十字链表。
邻接表
邻接表存储图的实现方式是,给图中的各个顶点独自建立一个链表,用节点存储该顶点,用链表中其他节点存储各自的临界点。
但是非常不方便同时计算出度和入度。
十字链表
十字链表实质上就是为每个顶点建立两个链表,分别存储以该顶点为弧头的所有顶点和以该顶点为弧尾的所有顶点。
- tailvex:用于存储以首元节点为弧尾的顶点位于数组中的位置下标;
- headvex:用于存储以首元节点为弧头的顶点位于数组中的位置下标;
- hlink 指针:用于链接下一个存储以首元节点为弧头的顶点的节点;
- tlink 指针:用于链接下一个存储以首元节点为弧尾的顶点的节点;
- info 指针:用于存储与该顶点相关的信息,例如量顶点之间的权值;
代码
#define MAX_VERTEX_NUM 20#define InfoType int//图中弧包含信息的数据类型#define VertexType inttypedef struct ArcBox{int tailvex,headvex;//弧尾、弧头对应顶点在数组中的位置下标struct ArcBox *hlik,*tlink;//分别指向弧头相同和弧尾相同的下一个弧InfoType *info;//存储弧相关信息的指针}ArcBox;typedef struct VexNode{VertexType data;//顶点的数据域ArcBox *firstin,*firstout;//指向以该顶点为弧头和弧尾的链表首个结点}VexNode;typedef struct {VexNode xlist[MAX_VERTEX_NUM];//存储顶点的一维数组int vexnum,arcnum;//记录图的顶点数和弧数}OLGraph;int LocateVex(OLGraph * G,VertexType v){int i=0;//遍历一维数组,找到变量vfor (; i<G->vexnum; i++) {if (G->xlist[i].data==v) {break;}}//如果找不到,输出提示语句,返回 -1if (i>G->vexnum) {printf("no such vertex.\n");return -1;}return i;}//构建十字链表函数void CreateDG(OLGraph *G){//输入有向图的顶点数和弧数scanf("%d,%d",&(G->vexnum),&(G->arcnum));//使用一维数组存储顶点数据,初始化指针域为NULLfor (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->xlist[i].data));G->xlist[i].firstin=NULL;G->xlist[i].firstout=NULL;}//构建十字链表for (int k=0;k<G->arcnum; k++) {int v1,v2;scanf("%d,%d",&v1,&v2);//确定v1、v2在数组中的位置下标int i=LocateVex(G, v1);int j=LocateVex(G, v2);//建立弧的结点ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));p->tailvex=i;p->headvex=j;//采用头插法插入新的p结点p->hlik=G->xlist[j].firstin;p->tlink=G->xlist[i].firstout;G->xlist[j].firstin=G->xlist[i].firstout=p;}}
邻接多重表
邻接多重表存储无向图的方式,可看作是邻接表和十字链表的结合。同邻接表和十字链表存储图的方法相同,都是独自为图中各顶点建立一张链表,存储各顶点的节点作为各链表的首元节点,同时为了便于管理将各个首元节点存储到一个数组中。
注意,邻接多重表仅适用于存储无向图或无向网。
