数据结构期末笔记[自用]

这是数据结构的期末复习笔记

要期末考试了, 总结了一些笔记. 写的很无趣还请谅解, 适合有一定基础的同学. 同时lz还没有找到合适的图床, 因此这篇笔记有些地方并没有配图


数据结构概论

每个程序都分为数据结构+算法,其中数据结构又分为逻辑结构和存储结构.
可以粗略将他们分为线性表,顺序表和……
每个存储结构的附属使用函数都应该包含:

ADT LinearList {
Data element: D = { ai | ai属于ElemSet,  i = 1,2,3,,n, n≥0}// 每个存储信息的节点
Relationship: R = {<ai-1,ai> | ai-1,ai 属于 D, i = 2,,n}
Operations:// 节点间的联系
initList(L)// 初始化结构,一般包含向系统申请空间和初始化除了节点信息外的所有变量
createList(L)// 初始化所有节点变量
getLen(L), // 得到当前使用节点的数量
locateElem(L,x)// 得到L中节点x的位置信息
               // 如果x不在L中则返回0或-1
getElem(L,i,e)// 将L中第i个节点信息存在临时变量e中, 1≤i≤n
insertElem(L,i,x)// 将变量x插入在L中第i个节点前, 1≤i≤n+1
deleteElem(L,i,e)// 删除L中的第i个元素, 1≤i≤n
outputList(L)// 输出整个链表存储的信息
} ADT LinearList

一般来说,L的结构体基础包含:

typedef  int ElemType; // 储存的信息类型,方便后面统一修改
#define INITSIZE 100 // 初次创建的节点数量
typedef  struct
{  
    ElemType  *data;
    int length;// 当前的已被使用的节点数量
    int listsize;// listsize=INITSIZE,如果没有元素超出列表时申请新的空间增加列表数量可以不包含这个
}sqlist;

以及配套函数:初始化列表,插入节点,删除节点,得到节点信息,获得列表信息

栈和队列 Stack & Queue

栈是头进头出的数据结构,队列则是头进尾出,两者都能使用数组和链表来实现.

优势

请注意:还请把栈和队列看作一种动态的数组,他们的设计是为了存储各种用途的中间过程待操作的变量.

  • 比如说 在递归函数中将第一层递归存入栈底……最后一层递归存入栈顶,然后弹出栈顶和倒数第二层递归进行计算,再弹出倒数第三层和前面的结果计算…一直弹出直到栈空.
  • 又比如说队列 在缓冲处理中将全部待处理的操作按顺序入队列,每次弹出一个解决一个.

总结一下:

  • 栈的优势比较抽象,从结果看他的操作顺序和入栈顺序是反过来的,可以用于递归和反转别的存储结构,从过程看使用栈来进行计算比较抽象
  • 队列的优势就在于他先进先出的数据结构与逻辑结构相吻合,选择适用的场景就不需要更多的架构设计

结构

栈的结构体包含:

Definition of Sequential stack: 
#define INITSIZE 100 
 typedef int ElemType;
 typedef struct 
 {
    ElemType *base; /*栈底指针,你可以看作一个数组*/
    int top; /*栈顶指针,他的数值就是已经存了多少个元素 ,此时栈顶元素在base[top-1];*/  
    int stacksize; /*栈的内存大小*/
}sqStack;

基础函数包含:

initStack(S)// 初始化栈结构
getLen(S)// 获得栈此时的使用长度
getTop(S,x)// 获得栈顶元素并保存在x
pop(S,x)// 将元素x推出S栈顶(包含删除x)
push(S,x)// 将元素x放在S栈顶
isEmptyStack(S)// 检查栈是否为空
outputStack (S)// 输出栈中的所有元素
destroyStack(S)// 释放栈的内存

如果想高效利用空间可以设计一个长数组,两头各有一个栈


队列的结构体包含:

#define MAXCSIZE 100
typedef int ElemType;
typedef struct
{
     ElemType *base;// 初始化队列
     int front;// 队列下一个出去的元素 
     int rear;// 队列进入处 
}cqueue;

基础函数和栈差不多,注意rear指针的操作在(rear>MAXCSIZE)时rear=0防止内存假溢出即可.只有front和rear两指针相遇且不为空时为真内存溢出.

getLen(S)=(cq->rear – cq->front + MAXCSIZE)%MAXCSIZE;

串 String

其实串的存储结构就是字符串,基础函数请参考<string.h>.
这里只介绍一个核心KMP算法–定位串b在串a中的哪个位置

很简单,先给出代码:

int KMPindex(string s,string t,int pos)
{  int i,j,*next;
    if(pos<1||pos>s.length||pos>s.length – t.length+1) 
            return 0;
   
     next =(int *)malloc(t.length*sizeof(int));
     getNext(t,next);
     i=pos-1; j=0;
     while(i<s.length && j<t.length)
           if(s.ch[i]==t.ch[j] || j==-1)
              { i++; j++; }
           else
               j= next[j];
           if(j==t.length)
         return i-j+1; 
     else // i==s.length,搜索到s尾但仍不匹配,失败
         return 0;
}

算法的时间复杂度为O(n+m),具体思路为:

  1. 对模式链(待匹配链)进行分析,将模式链中的重复部分标注出来,每个位置的数字代表: 如果这里的字母与目标链(匹配链)不同,则返回至模式链[数字]重新与这里的字母匹配
  2. 进入while循环匹配, i代表目标链匹配进度,j代表模式链匹配进度. 如果俩字母匹配则i++,j++.如果俩字母不匹配则i不变,j=next[j] (或j-1,具体看next算法) 继续匹配

数组, 矩阵与广义表

这些都是大家很熟悉的存储结构了,因此只介绍一些冷门点

  1. 矩阵的存储结构与基础函数:
    可以使用二元数组,单或多方向链表也可以,这里我们使用三元组链表存储.
#define    MAXSIZE    100
typedef   int  ElemType;
typedef   struct  node{ 
     int  i,j;
     ElemType e;
} tupletype;

typedef struct
{
     tupletype   data[MAXSIZE]; 
     int rownum; /*行数*/
     int colnum; /*列数*/
     int nznum; /*表长,即非零元素个数*/
     int rpos[MAXSIZE];// 这个数组存储该元素位于data数组的第几个元素  可用于快速查找
}table; /*三元组表类型名*/

在求转置矩阵时只需要 (1) 将行和列的数值反转 (2) 重新按行或列排序从0到高

  1. 稀疏矩阵:
    指的是非零元素数量占整个矩阵大小的比例小于0.05的矩阵. 在存储时仅用 (横坐标x,纵坐标y,数据) 的三元组储存,需要用矩阵的逻辑结构时再组装矩阵

  1. 广义表
    广义表是个很玄乎的东西,可以把它理解为离散数学中的 集合 . 一个广义表中可以存储:单个元素,几个元素的集合,几个集合,别的广义表甚至包括他自己.
    广义表的长度看它最外层包含几个元素,不管是广义表集合还是单个元素都算1. 而广义表的深度则看他最多内含多少层括号


    EX:下面有5个广义表: A=( ) B=(b, c) C=(a, B) D=(B, C, A) E=(a, E) 广义表D的长度为2,深度为3, 广义表E的长度为2,深度为无穷大 是的长度不看展开后的广义表, 但深度要看展开后有多少层括号


    广义表的简单函数: 取表头head(GL): 返回广义表的表头元素 和取表尾tail(GL): 返回广义表的除了表头元素外 的所有元素 组成的一个广义表
    还是例子: L2 = ((a,b), (c,d)) , getHead (L2) = (a,b) ; getTail(L2) = ((c,d)) ; getTail(getHead(L2)) = (b) ;

树 Tree

树的基本性质 (术语)

  • 度(Degree): 一个节点有n个 直接链接的 子节点, 度即为n.
  • 叶子节点: 深度为零 即没有子节点 的节点
  • 一棵树的高度(Height): 一棵树可以最多下探 几次 +1. 请注意这条链路上有n个节点高度就为n

常见常错的完全二叉树

指的是每个节点的度数最多为2, 高度为n的树有(2n-1)到(2n-1)个节点. 同时, 每个节点都是从左到右排序的.

  1. 对于每个节点i , 他的左节点为2i , 右节点为2i+1 , 父节点为 (i/2 向下取整)
  2. 对于有n 个节点的完全二叉树:
  • 他的高度 k = (log2向下取整) + 1 = (log2向上取整)
  • 叶子节点的数量为 n - (n/2向下取整)
  • 度为2的节点个数为: n - 叶子节点 - 度为1的节点数(为1或为0)

二叉树的存储和遍历

无非是用二叉链表, 结构体包含左节点指针, 存储元素, 右节点指针. 或者使用三叉链表, 多包含一个指向父节点的指针

基础函数包含:

  • 先/中/后序 (pre/in/post order) 遍历二叉树 (注意不是完全二叉树也是可以遍历的) : 这里的先中后都是相对于根节点来说的.比如说先序遍历的代码如下: 先访问根节点, 然后是左右节点
void preOrder (bitTree *bt) {// 先序遍历         
   if (bt!=NULL) {
      printf(%c->,bt->data) ; 
      preOrder(bt->lchild); 
      preOrder(bt->rchild);
   }
}

中序指先访问左节点再是根节点最后右节点, 后序指左右中的顺序.
在写遍历的输出结构时回想一下上面讲的栈的函数递归调用, 只有一个递归函数被递归结束才会进入到下一条代码.
每一种遍历顺序对于唯一的树都有唯一的的输出结果, 但是一种输出结果可以对应多种树. 一种方便的还原方法就是看父节点位于哪里, 在中序遍历中父节点左边的是其左节点, 右边的是其右节点


EX: inorder = g d h b e i a f j c preorder = a b d g h e i c f j

我们首先观察到在中序遍历中gdhbei在根节点a的左边, fjc在其右边, 将其分为两坨. 然后在先序遍历中a后面直接跟着b再跟着d, 意味着a的左节点是b,b是d的父节点……

哈夫曼树

简单来讲就是一种 将各个带有权值的节点 构建成一颗最优质的二叉树就叫哈夫曼树

那么怎么构建呢?

权值的计算就是每个节点的权值 x 每个节点的高度的求和
因此构建哈夫曼树就是要构建权值最小的树.
但是话又说回来了, 如果所有节点一股脑的塞在一起树里, 你应该如何去储存他们的位置呢?

哈夫曼树的储存与读取

哈夫曼树权值最小是为了越大的数据就意味着读取次数越多就应该越被快速读取. 那么我们应该怎么读取呢?

举个例子, 如果我们需要将一个存满各种数字的文本压缩缩小其大小(其特点是有各种奇怪的数重复出现). 最简单的就是简化储存方法对吧, 假设我们创建了一棵饱和全是数据节点的哈夫曼树, 当我们用1和0去表示每个节点在哈夫曼树中的位置时: 比如说:0110, 我们应该怎么区分这是两个节点011和0还是01和10呢?
因此哈夫曼树必须有过程节点, 将所有过程节点都放在父节点的右节点用0来表示便是一颗最简单的哈夫曼树.
在储存时先存入构建的哈夫曼树的信息, 再存入编译后的每个节点的信息, 在读取时反过来即可.


哈夫曼树的构建

直接上代码:

// 构建霍夫曼树
Node* build_huffman_tree(int* frequencies) {
    Node** nodes = (Node**)malloc(BYTE_COUNT * sizeof(Node*));
    int node_count = 0;
    if (nodes) {
        // 创建初始节点
        for (int i = 0; i < BYTE_COUNT; i++) {
            if (frequencies[i] > 0) {
                nodes[node_count++] = create_node(i, frequencies[i]);
            }
        }
        // 使用优先队列构建霍夫曼树
        while (node_count > 1) {
            qsort(nodes, node_count, sizeof(Node*), compare_nodes);
            Node* left = nodes[node_count - 2];
            Node* right = nodes[node_count - 1];
            Node* merged = create_node(0, left->freq + right->freq);
            merged->left = left;
            merged->right = right;
            nodes[node_count - 2] = merged;
            node_count--;
        }
        Node* root = nodes[0];
        free(nodes);
        return root;
    }
    return create_node(0, 0);
}

// 生成霍夫曼编码,并验证正确性
void generate_huffman_codes(Node* root, char* code, int depth, char codes[BYTE_COUNT][MAX_TREE_HT]) {
    if (root == NULL) return;
    // 叶节点,保存编码并验证
    if (root->left == NULL && root->right == NULL) {
        code[depth] = '\0';
        // 打印编码进行验证
        printf("Byte %d code: %s\n", root->data, code);
        strcpy(codes[root->data], code);
        return;
    }
    // 左子树:'0'编码
    code[depth] = '0';
    generate_huffman_codes(root->left, code, depth + 1, codes);
    // 右子树:'1'编码
    code[depth] = '1';
    generate_huffman_codes(root->right, code, depth + 1, codes);
}

在设计哈夫曼树只需要记住一点: 每个节点最后的编码一定是A…AB的结构, 识别到B的时候往后推有几个A就能找到节点.

一般的树与森林一说 Trees and Forests

此时你并不知道每个节点有几个孩子节点, 因此不能用简单的结构体定制节点结构了. 我们这里采用链表:

#define MAX_TREE_SIZE  100
typedef struct PTNode 
{
      Elem  data;
      int   parent;   
 } PTNode; 
typedef struct
{ 
       PTNode  nodes[MAX_TREE_SIZE];
       int    r, n;      // the index of root and the number of nodes
} PTree;

每个节点包含: 他们自己的数据 和 父节点的数字.

  • 如果你想优化你的储存方法可以在同一层的节点间储存兄弟节点的数字 (孩子链表表示法)

图 Graph

图分为无向图与有向图, 无向图每个节点仅有度的概念, 有向图有出度和入度.

图的基本概念

  1. 连通分量: 一张图中的多个最大连通子图.
  • 最大连通子图: 任意两个顶点都是连通的. 你不能通过增加任何其他顶点或边来使其更大而保持连通性
  1. 生成树/生成森林: 包含图中全部顶点的一个极小连通子图. 对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
  2. 简单路径/简单回路: 在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。

图的存储

你可以使用邻接矩阵 (即表示横坐标x->纵坐标y的关系) 和邻接表 (使用多个链表直接储存每个节点指向别的什么节点)
比较进阶一点的就是十字链表与多重邻接表:

十字链表
其实就是每个结构体多了一个指针储存什么点指向它, 在使用时配合数据结构并查集 可以快速搜索.


在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低.

因此我们重新设计了多重邻接表
节点结构
其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。
弧节点结构
ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。
多重邻接表
注意, ilink指向的结点的jvex一定要和它本身的ivex的值相同

图的遍历

深度优先遍历 DFS
bool visited[MAX_VERTEX_NUM];	//访问标记数组
/*从顶点出发,深度优先遍历图G*/
void DFS(Graph G, int v){
	int w;
	visit(v);	//访问顶点
	visited[v] = TRUE;	//设已访问标记
	//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
	//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
	for(w = FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w)){
		if(!visited[w]){	//w为u的尚未访问的邻接顶点
			DFS(G, w);
		}
	}
}
/*对图进行深度优先遍历*/
void DFSTraverse(MGraph G){
	int v; 
	for(v=0; v<G.vexnum; ++v){
		visited[v] = FALSE;	//初始化已访问标记数据
	}
	for(v=0; v<G.vexnum; ++v){	//从v=0开始遍历
		if(!visited[v]){
			DFS(G, v);
		}
	}
}
广度优先遍历 BFS
/*邻接矩阵的广度遍历算法*/
void BFSTraverse(MGraph G){
	int i, j;
	Queue Q;
	for(i = 0; i<G,numVertexes; i++){
		visited[i] = FALSE;
	}
	InitQueue(&Q);	//初始化一辅助用的队列
	for(i=0; i<G.numVertexes; i++){
		//若是未访问过就处理
		if(!visited[i]){
			vivited[i] = TRUE;	//设置当前访问过
			visit(i);	//访问顶点
			EnQueue(&Q, i);	//将此顶点入队列
			//若当前队列不为空
			while(!QueueEmpty(Q)){
				DeQueue(&Q, &i);	//顶点i出队列
				//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
				//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
				for(j=FirstNeighbor(G, i); j>=0; j=NextNeighbor(G, i, j)){
					//检验i的所有邻接点
					if(!visited[j]){
						visit(j);	//访问顶点j
						visited[j] = TRUE;	//访问标记
						EnQueue(Q, j);	//顶点j入队列
					}
				}
			}
		}
	}
}

最小生成树 Minimum-Spanning-Tree, MST

指定一个节点A, 从节点A出发访问所有其它节点且总权值最小的路径称为最小生成树

普里姆算法 Prim

从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作
Prim
请注意, 每次选择边的集合都只是已经被连接的点的链接边的集合

克鲁斯卡尔 Kruskal

初始时为只有n个顶点而无边的非连通图T = V , T= {V, {}}T=V,,每个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T TT,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T TT中所有顶点都在一个连通分量上。
Kruskal

最短路径

指定一个起点一个终点, 求出经过这两点总权值最小的路径

迪杰斯特拉 Dijkstra

Dijkstra
简单来说就是, 每一轮都选定一个过程点, 然后更新新能到达的路径, 再选,在更新……的循环
这个算法旨在每一轮都选定局部最优解

弗洛伊德 Floyd

例子
然后依次选定中间节点, 对于所有顶点{ i , j },如果有A−1 [ i ] [ j ] > A−1[ i ] [ 0 ] + A−1 [ 0 ] [ j ],则将A−1 [ i ] [ j ]更新为A−1 [ i ] [ 0 ] + A−1 [ 0 ] [ j ]
计算过程

拓扑排序

一条从头指向尾的序列,每个节点都直接指向下一个节点
算法也非常的简单:
拓扑

关键路径

关键路径和最短路径有点相反, 在创建的图中, 有且仅有一个入度和出度为0的点, 从源点到汇点之间的最长路径就是关键路径
在实际生活中的含义就是: 完成这项工程所需的最短完成时间
在这个特殊的图中: 每条边是一次活动, 其权值就是完成它需要的时间. 每个节点是一个状态, 达成这个状态才能开展后面的活动.
在特殊的图中有如下性质:

  • 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
  • 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
    AOE图
求关键路径的算法

重要的概念及参数:
1.事件的最早发生时间ve:即顶点Vk的最早发生时期。
2.事件的最晚发生时间vl:即顶点Vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期。
3.活动的最早开始时间e:即弧ai的最早发生时间。
4.活动的最晚开始时间l:即弧ai的最晚发生时间,也就是不推迟工期的最晚开工时间。
5.一个活动ai的最迟开始时间 l ( i ) 和其最早开始时间 e ( i )的差额d ( i ) = l ( i ) − e ( i ):它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称l ( i ) − e ( i ) = 0 即表示ai的活动是关键活动


具体的算法步骤如下:

  1. 从源点出发,令ve ( 源点 ) = 0, 按拓扑排序求其余顶点的最早发生时间ve ( ) 。
    • 请注意, 再求最早发生时间时需要所有入边在最短时间内完成才能记录时间
  2. 从汇点出发,令vl ( 汇点 ) = ve ( 汇点 ),按逆拓扑排序求其余顶点的最迟发生时间vl ( ) 。
    • 在这个函数中需要用前一步所求的最短完成所有活动的时间倒减去前面的边. 同时, 当一个节点有多条出边时, 需要用指向的节点的最迟完成时间减去活动时间与别的出边时间比较.
    • 这里最好使用递归函数从逆拓扑序列往前推,如果有点后面的最晚完成时间比之前求的更小则需要更新数据
  3. 根据各顶点的ve ( )值求所有弧的最早开始时间e( ), 根据各顶点的vl ( )值求所有弧的最迟开始时间l( )。
    • 在这个函数中, 弧ai的最早发生时间就是其起点的 ve(i) , 最短发生时间就是其指向点(终点)的 vl(i) 减去弧ai的权值.
  4. 求AOE网中所有活动的差额d( ), 找出所有d ( ) = 0的活动构成关键路径。

查找

在这里先给出这章的关键词: 顺序查找、折半查找和索引查找; 二叉排序树(BST); 二叉平衡树; B-树, 哈希表;

简单的查找

顺序查找就是for或者while循环的单方向遍历数组;
折半查找就是不断取数组中间的元素然后用以和目标数比较, 然后再取一半……循环

  • 折半查找的查找成功率要除以n, 不成功率要除以n+1. 因为查找不成功还涉及最后一次折半查找元素后的比较

EX: 现在有一个从小到大的1到12的数组, 对他进行折半查找, ASL(average search length )SUCC = (11+22+34+44)/11=3, ASLUNSUCC = (34+48)/12=11/3;
索引查找就是在储存元素的数组之外有一个附表, 在附表中分别储存了把整一块表分为好几块子表, 每块表的头元素的元素 以及 每一块中的最大元素.

  • 在查找时, (1) 先用附表记录的最大元素与目标数比较确定在哪一块子表中 (2) 再用附表存储的位置开始遍历子表

二叉搜索树 BST(Binary Search Tree)

此刻我们的目标是将一颗有很多空心点的树变成一颗饱和的, 高度尽量小的树.
最后这棵树应该满足: 每个节点的左子树的每个点均小于其本身, 右子树均大于其本身. 同时 每个节点的左子树和右子树的深度之差的绝对值要小于2.
每一次你的旋转都应该找到最底层的不平衡点,然后旋转包含他在内的树.

B-树

n阶B树指的是每个节点最多拥有n个子节点, 其中用指针指向子节点,在每个指针之间存储一个数据, 即每个节点最多存储n-1个元素.

哈希表

对于哈希来说最重要的是哈希函数的选择和你怎么存放哈希值相同的元素.

排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值