线性表

栈

stack一种 FILO first in last out(先进后出)的数据结构,常用于递归问题求解

常用场景:数制转换、迷宫求解、树的个数、括号匹配、表达式求值等等

先采用数组模拟的方式看看栈容器的实现:

int st[N];
// 这里使用 st[0] (即 *st) 代表栈中元素数量,同时也是栈顶下标
// 压栈 :
st[++*st] = var1;
// 取栈顶 :
int u = st[*st];
// 弹栈 :注意越界问题, *st == 0 时不能继续弹出
if (*st) --*st;
// 清空栈
*st = 0;

如何理解栈?

举个例子,拿字符串的左右括号匹配问题说明(给定一串字符串,只包含[]、{}、()这三种类型的括号,是否合法匹配),这道问题可以依靠栈的思想来解决。

思路:右括号都需要寻找离他最近的、还没有匹配的左括号,这里栈这个数据容器只存放还没等到右括号的左括号,因为我们习惯于自左向右进行匹配,而偏偏最需要注意的右括号可能都在右侧存放,很多层次的左括号也不能这样扔掉,所以栈就需要这样的存储左括号(栈在这里就是一个等待匹配的清单),如最近一次放进去的左括号应该被第一次遇到的右括号进行匹配(这就是后进先出)

匹配的过程:只有当关闭括号即右括号出现时,才会进行匹配。有这样的几步

(1)先看栈是不是为空,若栈为空代表没有可以和当前右括号匹配的左括号,直接就是不合法匹配;栈不空进行下一步

(2)看栈顶是谁(合法匹配应该为:离当前括号最近的还没匹配的左括号)

(3)匹配的类型是否配对,配对继续进行后续的比较,否则代表不合法

(4)重复上述流程,在所有字符遍历过后,若栈不为空,说明没有匹配完(左括号多)代表失败,否则匹配成功

/**
	该函数实现的功能为:给定一个字符串,判断括号是否匹配
**/
bool isValid(string s) {
	stack<char> st;
    for(char c : s) {
        if(c == '(' || c == '[' || c == '{') st.push(c);
        else {
            if(st.empty()) return false;
            char x = st.top();
            if(x == '[' && c != ']') return false;
            if(x == '(' && c != ')') return false;
            if(x == '{' && c != '}') return false;
            st.pop();
        }
    }
    return (st.empty() ? true : false);
}

STL 栈的操作

stack<int> s;
// 新建两个栈 st1 和 st2
std::stack<int> st1, st2;
// 为 st1 装入 1
st1.push(1);
// 将 st1 赋值给 st2
st2 = st1;
// 输出 st2 的栈顶元素
cout << st2.top() << endl;
// 输出: 1

栈的使用都是一种基于角色的转变,比如一开始的括号匹配问题,栈充当 等待匹配 的角色,而进一步一个问题

相邻重复字符消除:删除字符串中所有相邻重复字符,反复删除直到不能删。如 abbaca → ca 这种栈的角色就是 最近有效状态 栈的存储就是已经“确认保留”的字符(把遍历到的每一个字符进行一个过滤筛选放入栈中)。

栈进阶:

(2)栈 = 嵌套结构的上下文

题目:字符串解码3[a2[c]] → accaccacc

思维:栈存的是进入内层前的外层状态,在遇到 ] 时恢复

队列

queue一种 FIFO (first in first out)先进先出的数据结构

queue<int> q;    // 普通队列
deque<int> q;    // 双端队列(存在前后双指针)
priority_queue<int,vector<int>,greater> q;      // 优先队列

链表

按值传递:int、long、byte、short、char、float、double、bool、string

按引用传递:自定义类、数组

按引用传递传递的参数是新拷贝一份地址和原地址指向同一个位置

单链表定义:节点在内存中存有数据和下一个节点的指针

双向链表:在单链表基础上,增加一个prior指针指向上一个节点的指针

在地址划分上,链表分为数据域、指针域

头节点 指针 指向第一个节点的地址

链表的基本操作:

// 头文件
#include <list>  // 双向链表
#include <forward_list>   // 单向链表
//单向链表
#include <forward_list>  
std::forward_list<int> fl = {1,2,3};
fl.push_front(0);      // O(1)
fl.insert_after(fl.begin(), 42);
fl.remove(2);          // O(n)

//双向链表
#include <list>
std::list<int> lst = {1,2,3};
lst.push_back(4);
lst.insert(std::next(lst.begin()), 42);
lst.erase(lst.begin());

纯 C 实现的链表

// 数据结构定义 ( 非嵌入式 )
struct node {
    Type data;
    struct node* next;
};
// 嵌入式
struct node {
	Type data;
	struct link_node;  // 链表管理器
}
struct link_node {
    struct link_node* pre;
    struct link_node* next;
};
// 操作定义(侵入式为例)
// 尾插
#define Add_tail(head,node) 	\
	(node)->next = (head);		\
	(node)->pre = (head)->pre;	\
	(head)->pre->next = head;	\
	(head)->pre = node;
// 头插
#define Add_head(head,node)		\
	

1、反转链表

1—–> 2 ——> 3 ====> 3—-> 2—–> 1

头节点 变成3 ,1的next指向null

ListNode reverseList(ListNode head){
    ListNode pre = null;
    ListNode next = null;
    while(head != null){
        //next指针往前走
        next = head.next;
        //head下一个指针域指空
        head.next = pre;
        //pre指向head
        pre = head;
        //head指向下一个
    	head = next;
    }
}

串

模式串匹配

BF

string s,t;
for(int i = 0, j = 0; i < s.size() && j < t.size();){
	if(s[i] == t[j]) {
        ++i,++j;
    } else {
        i = i - j + 2;
        j = 0;
    }
    if(i == s.size() - 1 && j == t.size() - 1){
		// 匹配开始位置
        i - t.size();
    }
}

KMP

树型结构

基本概念

树是由一个或一个以上的节点组成的。树中存在一个特殊节点(树根 // 根节点),每个节点都是由一些数据和指针组合成的记录。每个节点本身也是一种树结构(即此根节点子树),一棵合法的树,节点间可以互相连接,但不能形成无出路的回路。

img

基本术语:

1、度数(degree):每个节点所有子树的个数,如A的度数为3,树的度【树中节点最大的度数】

2、层数(level):设A层数为1,则B、C、D层数为2

3、高度(height):树的最大层数,如图高度为4

4、树叶【或 终端节点】:度数为0的节点就是树叶,如图中K、L、F

5、非终端节点:树叶以外的节点,A、B、C、D、E、H

6、有序树【无序树】:树中节点的各子树从左到右为有序树,否则为无序树

7、森林:n课互不相交树的集合。将一棵树根节点删去就是森林,反之,给n课独立的树加上一个节点,并把这n课树作为该节点的子树,森林就变回了树。

性质:

1.树的节点数 = $ m+1$,其中 $m$ 为树的边数

2.度为 $m$ 的树中第 $i$ 层 最多有 $m^i$-1 个节点

3.高度为 $h$ 的 $m$ 叉树 最多有 ($m^h$ -1)/(m-1)个节点

4.具有 $n$ 个节点的 $m$ 叉树 $min(h)=log_m(n*(m-1)+1)$

二叉树遍历

二叉树的遍历是指 从根结点出发,按照某种次序一次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次

1、前序:根左右

先访问根节点,再递归访问左节点,最后递归访问右节点

image-20250804215704843
void dfs(int x)
{
    if (a[x] == -1) {
        return;
    }
    cout << a[x] << " ";
    dfs(2 * x + 1);
    dfs(2 * x + 2);
}

2、中序:左根右

image-20250804220034495
void dfs(int x)
{
    if (a[x] == -1) {
        return;
    }
    dfs(2 * x + 1);
    printf("%d ", a[x]);
    dfs(2 * x + 2);
}

3、后序:左右根

image-20250804220049160
void dfs(int x)
{
    if (a[x] == -1) {
        return;
    }
    dfs(2 * x + 1);
    dfs(2 * x + 2);
    printf("%d ", a[x]);
}

序列建树

对于给定的遍历序列如:前序 + 中序、后序 + 中序 两种遍历方法可以推出唯一的树,构建方法

特殊二叉树

  • 满二叉树:深度为 $k$,且节点数为 $2^k-1$ 的二叉树

  • 完全二叉树:深度为 $k$,有 $n$ 个节点的二叉树,且自上而下,自左至右的每一个节点都与深度为k的满二叉树中编号对应

完全二叉树性质:

1、叶子结点只用出现在最下两层
2、最下层的叶子一定集中在左部连续位置
3、倒数两层若有叶子结点,一定都在右部连续位置
4、如果结点度为1,则该结点只有左孩子
5、同样结点数的二叉树,完全二叉树的深度最小

节点 $i$ 父亲节点公式:$(i - 1)/ 2$ ,0 的父亲节点是自己

左儿子:$2\times i+1$ 右儿子:$2 \times i+2$

哈夫曼树及编码

编码原则:不出现重复前缀,如(01、011 就是重复的前缀)

哈夫曼编码:在不出现歧义的情况下编出的码长度一定是最短的

构建哈夫曼树:

1、统计各个字符出现的次数

2、将字符的出现次数最少的开始,合并成一个父节点,继续和出现次数第二小的字符合并节点操作……

3、将边标号(左 0 右 1)

4、从根节点开始到需要编号的节点经过的路径,如A:01,B:010 ,C:00,D:011

该字符串的编码:1010110001100(13)可能 一定是最短的编码长度

  • 出现次数越多的字符编码越短
  • 哈夫曼树不存在奇数度节点
image-20250512141309499

堆(完全二叉树)

大根堆:这个结构中的任何一个子结构最大值都必须在顶部,即最大值为根节点

从整棵树的最后一颗子树开始调整,每次都让根节点和左右孩子去比较,如果根节点比左右孩子的最大值要小,那么就将这两个值进行交换,然后此时这颗子树变成了大根堆

小根堆:在这个结构中,改变当前位置的值

父亲节点:(i - 1) / 2

左孩子 : i * 2 - 1 右孩子 : i * 2 + 1

//down
void InitialHeap(int parent,int end){
    //大根堆
	int child = parent * 2 + 1;
	while(child < end){
		if(child+1 < end && a[child+1] > a[child]) child++;
		if(a[child] > a[parent]){
			swap(a[child],a[parent]);
			parent = child;   
			child = 2 * parent + 1;  
		}else{
			break;
		}
	}
}
for(int parent=n-1;parent >= 0; parent--){
		InitialHeap(parent,n);
	}
//堆的up
void upHeap(int index){
    while(heap[index] > heap[(index-1) / 2]) {
        swap(heap[index],heap[(index-1) / 2]);
        index = (index - 1) / 2;
    }
}

堆的应用:对顶堆(解决第k小/大的问题)、优先队列(频繁插入且有序的序列)

对顶堆:可以动态维护一个序列上第k大的数,k值会发生变化。

对顶堆由一个大根堆和一个小根堆组成,小根堆维护前k大的数(包含第k大),大根堆维护小于第k大的数

操作:

1、插入:插入的元素 >= 小根堆堆顶的元素,将该元素插入小根堆,否则插入大根堆

2、维护:小根堆大小 > k 时,将小根堆堆顶元素取出插入大根堆,直到小根堆元素个数 = k

​ 小根堆大小 < k 时,从大根堆里取出元素插入小根堆,直到小根堆元素个数 = k

3、查询:查询第k大的元素,小根堆堆顶元素

4、删除:删除第k大元素,删除小根堆堆顶元素

image-20251015162536292
priority_queue<int> a1; // 大根堆
priority_queue<int, vector<int>, greater<int>> a2; // 小根堆
int main(){
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; ++i) {
        int x;
        cin >> x;
        if (a2.empty() || x > a2.top()) {
            a2.push(x);
        } else {
            a1.push(x);
        }
        while (a2.size() > k) {
            a1.push(a2.top());
            a2.pop();
        }
        while (a2.size() < k && !a1.empty()) {
            a2.push(a1.top());
            a1.pop();
        }
    }
    printf("%d ", a2.top());
    a2.pop();
    return 0;
}

二叉排序树BST

二叉查找树,又称二叉搜索树。特点为:当前结点值 > 左孩子值,当前结点值 < 右孩子值

还有一个特性就是 中序遍历 可以得出有序序列

用处:快速搜索的任务,动态查找

         (4)
         /  \
       (2)   (6)
      /   \  /  \
    (1)  (3)(5) (8)
                  \
                  (9)

BST的平均查找长度和 log(n)等量级,但有时需在构建过程中完成 平衡化 处理,成为AVL(实现了AVL的 BST称为 BBST)

查找:时间复杂度: $O(\log_2 n)$

  • 插入数据时保持大小关系,小于自己的放左边,否则放右边
  • 按照中序进行遍历,得到排序结果(DFS 实现)

BST的删除,时间复杂度: $O(\log_2 n)$

  • 删除的是叶子结点,直接删除即可
  • 若删除的结点只有左子树 / 右子树,直接让存在的左右子树代替
  • 若删除存在左右子树,找直接前驱 或者 后继 替换当前要删除的值,然后删除前驱或后继(原问题 ===> 删除左子树 / 右子树 / 叶子结点)

平衡二叉树 AVL/BBT

左子树和右子树深度之差绝对值不超过1,即 $| d_{l}-d_{r}| \le1$,其中 d 代表树高或者深度,且它的左右子树也为AVL

平衡因子(BF):| 该结点左子树 - 右子树的深度 | ,取值为$[-1,0,1]$

在构建 AVL 树的过程中,会出现 BF 值不合法情况,需要进行调整,比如

 (4)     /* BF = -2,不合法值 */                 (6)                    
   \                       进行调整             /  \
     (6)                     ==>              (4)  (8) 
     /  \                                       \
    (5) (8)                                      (5) 

AVL 树存在 4种调整,分别是单向左旋、右旋,双向旋转(先左后右、先右后左)

判断针对哪一个结点进行旋转操作:找到最小不平衡子树(即以当前结点为根节点时,$|BF| \ge 2$)

(1)单向左旋(RR)

   (4)                                        (6)                    
  /   \                                       /  \
(3)    (6)                  ==>              (4)   (8) 
       /  \                                 / \
      (5) (8)                             (3)  (5)
          / \  // 若在此处插入结点,|BF| >= 2,则需要进行调整,正常4节点要变为6的左孩子,但和原本5冲突,因此冲突的左孩子变为右孩子,如右图

(2)单向右旋(LL)

     (6)                                    (3)                    
    /   \                                  /    \
  (3)    (8)             ==>             (4)     (6) 
  / \		                                     / \    
(4)  (5)						        	 (5) (8)
/ \
// 若在此处插入结点,|BF| >= 2 ,则需要进行调整,正常6节点需要右旋变为3的右孩子,但原本的3存在右孩子,因此冲突的右孩子要变为左孩子,如右图

(3)双向旋转(对失衡节点左孩子先左旋RR,后对失衡结点右旋LL,LR)

     	  (8)                                     (8)                            (5)
         /   \                                   /   \                          /   \
        (3)   (9)                               (5)   (9)                     (3)    (8)
       /  \               ===>                 /  \          ===>            /  \    /  \
     (2)  (5)                                (3)  (6)                      (2)  (4) (6)  (9)
          / \                                / \                                / \
        (4) (6)                            (2) (4)                                 
        / \    /* 在此处插入结点 */             / \

(4)双向旋转(对失衡节点右孩子先右旋LL,后对失衡节点左旋RR,RL)

    (3)                                         (3)                                    (5)  
   /    \                                      /  \                                   /   \
(1)     (7)                 ==>              (1)   (5)          ==>                 (3)    (7) 
        /  \                                       /  \                            /  \    /  \
      (5)  (8)                                   (4)  (7)                        (1)  (4) (6)  (8)
      /  \                                       / \  /  \                            / \  
    (4)  (6)                                         (6) (8)
    / \  
   // 若在此处插入结点

接下来还有关于插入结点后进行什么样的平衡化的讨论,即当 BF 值分别在 -1,0,1 时该如何操作

在插入节点后,可能导致多个祖先结点失衡,只需要调整距离插入结点最近的失衡结点,其他结点自然平衡

     (3)  /* BF=-2 */                           (3)
	/  \     							    /   \
   (1)  (6)  /* BF=-2 (调整该结点)*/          (1)  (7)
          \              ==>                      /  \ 
           (7)                                   (6)  (9)
             \
              (9)

删除结点导致失衡,可能导致多结点失衡,需要沿着当前最近失衡结点调整,直到根节点

// 构建一棵 AVL [ 14 9 5 17 11 12 7 19 16 27]
       (11)
	 /    \
    (7)    (17)
   /  \    /   \
  (5) (9) (14) (19)
          /  \     \
         (12) (16)  (27) 

针对这个例子,删除结点5

       (11)                                   (11)  /* (11)BF=-2 失衡RR型 */       (17)
	 /    \                                 /     \                              /     \
    (7)    (17) /* (7)BF=-2 失衡RL型 */   (8)      (17)                         (11)     (19)
      \    /   \            ==>          /  \      /   \                       /   \     /   \
      (9) (14) (19)                     (7) (9)  (14)  (19)    ==>            (8)  (14) (16)  (27)
      /    /    /  \                              /    /  \                  /  \  /            \
    (8)  (12) (16)  (27)                        (12) (16)  (27)            (7) (9)(12)         (23)  
                    /                                      /
                   (23)                                   (23)

红黑树-RB树

背景:由于AVL树在插入时对于"平衡" 概念的严格要求,导致插入时可能效率较低,在实际工程中需要查找效率更高的解决办法,红黑树是一种基于BST和AVL树开发的,结点带有颜色(红、黑)的自平衡二叉排序树。通常 C++ 中的map、multiset,JAVA中的HashMap等数据结构底层为红黑树实现

性质:

  • 每个结点的颜色为红或黑的一种
  • 根节点,叶子结点(虚构的外部结点,NULL结点)一定是黑色
  • 任意两个红色结点不能相邻,即红色结点的孩子或父亲一定是黑色结点
  • 从根节点到 叶子节点的每条路径上的黑色节点数量相同

红黑树的查找:和BST一致,时间复杂度 $O(\log_2n)$

插入:相较于BST,红黑树需要考虑旋转和颜色问题,一般插入结点时,默认为红色。

  • 若插入时为空树,插入结点为根节点,将结点染成黑色
  • 若插入为非空树,父亲结点若为黑色,不需要任何操作;若父亲结点为红色,需要考虑插入结点的叔结点

(1)叔结点为红色:叔、爷、父结点变色,爷结点变为cur(变色后有相邻红色结点,以爷结点为基础,递归向上验证)

       (7)B								(7)R  /* cur,当前为根节点再次变色为 B*/
      /   \                               /   \
    (5)R   (8)R          ===>           (5)B   (8)B
    /                                   /
  (1)R /* 插入结点 cur */              (7)R

(2)叔结点为黑色:先通过AVL的四种旋转 LL、LR、RR、RL 以爷结点为准旋转,再将旋转点、旋转中心变色

注:RL、LR 需先进行一次旋转后,以最后一次的旋转点和旋转中心点为准变色

// RR型举例
       (7)B                                (9)B
          \                               /   \
           (9)R             ===>        (7)R   (12)R
             \
              (12)R  /*插入结点cur */
// LR型举例 
       (7)B                                (5)                        (5)B
      /    \             /* 旋转后*/      /   \      /*染色后 */     /    \
     (3)R   [NULL]B        ====>        (3)   (7)    ===>          (3)R    (7)R
       \
        (5)R  /* 插入结点cur */   

注:图中省略第一次旋转结果,第一次旋转后,是以5为旋转中心,7为旋转点,后续变色以此为基准

红黑树的删除:

  • 只有左子树 或 只有右子树,直接使用删除结点的左/右子树代替,代替后颜色变黑(防止违反黑路结点相同性质)
  • 左右子树都存在,找直接前驱或直接后继,用他们代替删除结点,问题转换为删除前驱或后继
  • 没有孩子,对于红结点:不会破坏黑路同结点数目一致性质,可直接删除;对于黑色结点:删除必然违反性质,引入双黑结点的概念(代表该路径缺少一个黑色结点),后面需要对该双黑结点转变为单黑结点
// 删黑色结点,兄弟为黑(兄弟孩子至少一红),LL型
	    (7)B                  (7)B                        (5)B
	   /    \                 /   \                       /    \
      (5)B   [双黑]   ==>   (5)B   [双黑]    ==>        (2)B    (7)B
     /   \                 /   \                              /   \
    (2)R   (6)R          (2)B   (6)R                         (6)R  [空结点]
// 删黑结点,兄弟为黑(兄弟孩子至少一红) LR型
       (7)B 								    (7)B                          (6)R
      /   \       /*兄孩染色父亲,父亲变黑*/       /   \      /* LR旋转*/        /    \
    (5)B   [双黑]     ==>                       (5)B   [双黑]   ==>            (5)B   (7)B
       \                                         \                                      \
       (6)R    							       (6)R                                  [NULL]单黑
// 删黑结点,兄弟为黑,兄弟孩子均为黑(兄弟变红,双黑上移)
           (15)B                          (15)B                             (18)B    
          /     \        /*双黑上移*/    /     \       /*RR型+染色*/        /     \
       (8)B       (18)B        ===>   (8)[双黑] (18)B     ==>            (15)B     (27)B
      /   \      /   \               /          /   \                   /   \      /    \          
    (6)B [双黑] (17)B (27)R        (6)R       (17)B  (27)R            (8)B  (17)B (25)B  (34)B
                      /  \                           /  \            /
                    (25)B  (34)B                  (25)B  (34)B      (6)R
// 删除黑结点,兄弟为红:兄父变色,父亲朝双黑旋转,保持双黑继续调整
         (18)B                  (18)R                     (15)B                  (15)B
        /     \                /     \                   /     \                /    \
      (15)R   [双黑]   ==>   (15)B   [双黑]   ===>     (9)B    (18)R   ===>   (9)B    (18)R 
     /    \                 /    \                            /    \                  /
   (9)B   (17)B           (9)B   (17)B                      (17)B  [双黑]           (17)R

注:若双黑结点遇到红色结点 或 根节点,可直接变为单黑

B-Tree

B-树 是一种平衡的多路查找树,在文件系统中很常用。对于m阶B-Tree,其有以下这些性质

  • 有序:结点内有序(任一结点元素,左元素 < 本身 < 右元素)
  • 平衡:所有的叶子结点都在同一层
  • 多路:最多 m 个分支,m - 1 个元素

​ $最少 \begin{cases} 根节点:2分支,1元素 \\ 其他结点:\lceil{\frac{m}{2}} \rceil 分支,\lceil{\frac{m}{2}} \rceil-1 \end{cases}$

B 树,将每个叶子结点下连接一些空结点(外部结点),而含有数据的结点称为内部结点

查找:和二叉搜索树类似,首先从根节点开始,依次比较左右子树元素,首先会先将该结点的元素加载到内存中,在该结点内部,可采用线性查找、二分查找的方式(大于当前元素,右移,直到找到不大于的元素查找它的左子树)

插入:先找到插入元素位置,若插入了没有发生上溢出(结点内元素 > m-1),无需调整;否则以中间元素 $\lceil{\frac{m}{2}} \rceil$ 为分裂点,将分裂点元素上移,两边元素分裂开成为两个独立结点。插入时可能导致一系列的上溢出(直到根节点)

删除:删除结点类似于BST,中序查找到直接前驱 / 后继,替换完后,问题转换为删除叶子结点。若叶子结点删除没有发生下溢出(结点内元素 < $\lceil{\frac{m}{2}} \rceil-1 $)直接删除,否则查看左右的兄弟是否可以借结点元素(父下,兄弟上),若两边兄弟都不够借,那么将它和左右兄弟之一合并。删除时可能出现一系列下溢出(直到根节点)重复判断即可,注意上层的下溢出调整后,它的左 / 右 子树 需要移动

$ 叶结点元素\begin{cases} 没有下溢出:无需调整 \\ 下溢出\begin{cases} 兄弟够借 \\ 兄弟不够借:合并(父亲下移到左,然后右合并过来) \end{cases} \end{cases}$

演示文档: B-Tree_BPlusTree_Demo.html


图状结构

分为:有向图、无向图

节点的度数 = 入度 + 出度

入度:指向当前节点的边的数量 出度:当前节点指向其他边的数量

图的存储

邻接矩阵

有向图:若节点1 连接 节点 2 ,则在矩阵中,第一行第二列标记为1 a[ 1 ] [ 2 ] = 1

无向图:若节点 1和 2相连,则 a[ 1 ] [ 2 ] = a[ 2 ] [ 1 ] = 1 对称性

若边带有权值 , 则在矩阵中 相连的边 设置为他们边的权值

即若1 、2节点相连,且 且边12所带权值为5,则 a[1] [2] = 5;

若是两个点之间不存在链接,将该边设置为正无穷表示

优点:容易判断两个顶点间是否有边,且易计算顶点的度

缺点:统计边的数量效率较低,空间复杂度高,最多点数量不能超1000

int spot1,spot2,val;
int matrix[500][500];
//带权有向图
for(int i=1;i<=n;i++){
    cin >> spot1 >> spot2 >> val;
    matrix[spot1][spot2] = val;
}

邻接表

$O(V + E)$ 空间存图

邻接矩阵唯一,邻接表不唯一

利用链表思想实现

//1、有向图
//遍历每个点的时候,里面都存有长度为n的链表
list<list<int>> graph;  //无权图
//空间上一开始链表都是空的,不占用空间,随着边的增加,空间开始增加

list<list<int[]>> graph; //带权图
相当于每次增加边时,都增加一个小数组,第一个值为要去的点,第二个值为权值
    
//2、无向图 == 特殊的有向图
将值加入对称的两条边,和邻接矩阵类似
//建图操作 -- 邻接表
struct Edge{
    int u,v,val;
    Egde *nex;
}epool[10001];
struct node{
    Edge *fir;  //出边链表的第一条边
}nodes[10001];
int k = 1; //从下标1的点开始记录
void add(int u,int v,int val){
    epool[k].u = u;
    epool[k].v = v;
    epool[k].val = val;
    epool[u].nex = nodes[u].fir;    //在一个点对应多条边时,指向之前插入的边的地址
    nodes[u].fir = &epool[k];     //更新边的地址,准备为下一条边赋值
    k++;
}
int main(){
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= m; ++i) {
        cin >> epool[i].u >> epool[i].v >> epool[i].val;
        add(u,v,val);
        //无向图
        add(v,u,val);
    }
    //遍历图
    Edge *e = &epool[id]; 遍历哪个顶点指向的边集
    while(e != nullptr) {
        cout << ......
        e = e->nex;
    }
}

链式前向星

链式前向星可以试着想象把邻接表顺时针旋转90度,然后把每个点下面挂着的动态数组想象成一个链表结构,结合给边编号的操作(输入时的顺序编号),head数组存的就是每个节点第一条边编号,也就是链表的头节点,然后next数组用来存放每条边的下一条边编号,也就是链表节点的next节点,直到遍历到0号边,相当于链表的nullptr,表示结束位置。然后每增加一条新的边,相当于找到每个节点的链表进行头部插入,将原来的头节点放到新的头节点的next里,然后把新的头节点挂到头上。

本质:模拟链表,采用头插方法,以边为单位,记录每一条边的目标点,包含有head和next头和边数组

以某个节点为起始节点的所有关联有向边的头插法

int head[N]; // 存储以i为起点的所有边的起始位置
int cnt = 0; // 记录边的数量
struct node {
	int to;    // 第i条边的终点
    int w;     // 第i条边的权值
    int nxt;   // 与第i条边相连的下一条边的位置
}edge[N];   //边数组

int add(int u,int v,int w){
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].nxt = head[u];
    head[u] = cnt++; 
}
image-20260624194014891

图的遍历

BFS、DFS

void dfs(){
	// 递归出口
    // 枚举调用自身,遍历所有情况
}

拓扑排序

源点:入度为0的点,在图中唯一; 汇点:只有一个出度为0的点

关键路径

哈希表

(1)、哈希地址:逻辑地址 <—- 哈希函数

(2)、哈希函数:由用户自己决定(取余法、直接定值法、平方取中法、折叠法【字符串】)

(3)、哈希冲突:哈希值相同时称为哈希冲突

哈希冲突解决方法:开放地址法、邻接表法

1、取余法

// 哈希函数  
H(key) =  key % p  ;p: 最大容量
eg:
以一个最大容量p = 7 的数组为例 a[0-6]
给出一组数据  a = [4,88,12,48,34,56]
H(a[i]) = ... ;  // 将算出的逻辑值放在对应数组下标下
H(4) = 4 % 7 = 4;
H(88) = 88 % 7 = 4;  // 哈希值相同时称为哈希冲突
//开放地址法
若哈希冲突,去其他空的地址下插入冲突的key
//邻接表法
在冲突的哈希地址下,建立一个链表

2、直接定值法

// 数据 = 地址  (较为浪费空间 数据最大需要开多大数组)

STL

字符串

常用string函数:length、size、push_back、append、find、substr(pos,len)截取、insert(index,str)、erase(index,count)、replace(first,last,str)、empty

(1)find 函数使用

原型说明
size_t find(const string& str, size_t pos = 0) const;查找子串 str,从 pos 开始
size_t find(const char* s, size_t pos = 0) const;查找 C 风格字符串 s
size_t find(const char* s, size_t pos, size_type n) const;查找 s 的前 n 个字符
size_t find(char ch, size_t pos = 0) const;查找单个字符 ch

(2)substr函数使用

basic_string substr(size_type pos = 0, size_type count = npos) const;
参数类型含义
possize_type起始截取位置的下标(从 0 开始计数)。默认值为 0
countsize_type要截取的字符数量(长度)。默认值为 npos

(3)getline 函数

// c++ 风格的函数,归属 <string>,可以指定输入方式,如 cin  标准输入等
// 形式 (1):使用默认分隔符 '\n'
istream& getline(istream& is, string& str);
// 形式 (2):显式指定分隔符 delim
istream& getline(istream& is, string& str, char delim);
//基础拼接
string s="";
s = s + "abc";   // s="abc"
//1、字符串切割   s.substr(p,n) :表示从s[p]开始往后截取n个字符,n可省略表示截取到最后
string s1("hello world!");
cout<<s1.substr(6,5);

//2、字符串查找   s.find(b,p) : 表示在字符串s中从p开始向后查找字符串b
//找到返回b的首字符地址,否则返回-1,p可省略表示从头开始查找
int p=s1.find(s2);

//3、字符串翻转  reverse(s.begin(),s.end());
string s2("abc");
cba;  // 输出

//4、插入字符
string s("ade");
s.insert(1,"bc");

int num = stoi(s);  // 将字符串转换成数值类型数据
reverse(s.begin(),s.end());  // 倒置字符串

vector容器

一维vector

#include<iostream>
#include<vector>
typedef struct{
	char company_name[100];
    double pizza_d;
    double pizza_weight;
}Piza;
int main(){
    vector<int> vi(4,10); //第一个参数申请的数组长度,第二个参数每个位置的元素赋初值10
    vector<int> vii(vi.begin(),vi.end());
    //1、添加元素
    vi.push_back(值);  //向vi[0] 赋值,尾插
    vi.insert(vi.begin(),4,2);  // 从vi开始的位置插入4个2
    //2、删除元素
    vi.pop_back()    //删除最后一个元素
    vi.erase(vi.begin()+i,vi.begin()+j)  //删除区间[i,j-1] 元素
	//3、访问元素
    vi.front() //访问第一个元素
    vi.back()  //访问最后一个元素
    //4、查找函数
    find(vi.begin(),vi.end(),x)  //x为需要查找的值
    }
    vector<Piza> arr(5);   //利用vector容器技术申请的结构体数组
// resize
// 1. 将容器大小调整为 count
void resize(size_type count);

// 2. 将容器大小调整为 count,新增的元素初始化为 val
void resize(size_type count, const value_type& val);
// v.unique(st, end);  工作机制
// 注:unique必须在整体有序的情况下使用
vector<int> a1;
a1 = [1, 2, 2, 3, 3, 5];  // 使用前
a1 = [1, 2, 3, 5, x, x];  // 使用后,返回值指针指向倒数第二个数,x(从这个数往后都变成无效值)
// 配合 v.erase(st,end);  删除左闭右开 [st , end) 元素
a1.erase(unique(a1.begin(),a1.end() ), a1.end());

二维vector

//创建二维数组
vector<vector<int>> matrix(3,vector<int>(3)) //创建3*3矩阵
for(int i=0;i<matrix.size();i++){    //二维vector行长度
    for(int j=0;j<matrix[i].size();j++) //获取二维vector的列长度
}

vector 操作

//1、复制与赋值
    //一个vector被赋值或复制时,会创建一个新对象,将所有元素深拷贝,对新vector修改不影响原来的vector
    vector<int> vec1 = {1,2,3};
	vector<int> vec2 = vec1;  //深拷贝
	vector<int> vec3 = move(vec1) //利用move(c++11开始)不用进行深拷贝,直接赋值
}
//2、查找最大元素
	int maxn = vec1.max_size();

//3、去重操作
vector<int> vec = {1,1,2,3,3,4};
vec.erase(unique(vec.begin(),vec.end()),vec.end());

//4、二分查找
//在这个区间上查找>=value的数,返回的是一个指针(迭代器)
lower_bound(起始位置,结束位置,value);
vector<int> vec = {3,7,2,9,11,8,15};
sort(vec.begin(),vec.end());
int t = lower_bound(vec.begin(),vec.end(),7); //查找第一个>=7的数
int t1 = upper_bound(vec.begin(),vec.end(),7); // 查找第一个 >7 的数

反向迭代器的使用

vector<int> ans;
auto last = find(ans.rbegin(),ans.front());  // 从尾部到头部查找第一个=ans.front() 的元素
// last 为反向迭代器,查找到的最末尾的元素
last.base()   // 将反向迭代器转为正向迭代器
// 公式 last = last.base - 1 即 last.base 迭代器指向原来迭代器后面一个元素

sort 接口函数

#include<algorithm>   //包含sort函数的头文件
typedef struct
{
    int a,b;
}node;
node pre[3];
bool cmp(int x,int y){
	return x>y;
}

bool copm(const node &x,const node &y){
    return x.a>x.a;
}

int main()
{
    int arr[5]={5,2,4,1,3};
	//第一个参数要排序的数组首地址,第二个参数要排序的数组尾地址,第三个参数,排序的方式
	sort(arr,arr+4,...) ;
    sort(arr+1,arr+4,cmp);   //按照  x>y的值来进行排序
    sort(pre,pre+3,comp);    //结构体数组 按其中某个成员之间的大小排序
}

运算符重载

在c++中,运算符重载本质上是定义一个函数,告诉编译器当运算符作用于结构体或类时,应该执行什么操作

不能重载的运算符: . 成员运算符、 :: 域运算符、 * 、-> 成员指针访问运算符、sizeof 长度运算符、?:条件运算符、预处理运算符 #

**仿函数:**如果类重载了operator 运算符,则这个类(结构体)可以称作仿函数

重载运算符函数可以对运算符作出新的解释,但原有基本语义不变:

1、不改变运算符的优先级 2、不改变运算符的结核性

3、不改变运算符所需要的操作数 4、不能创建新的运算符

//语法1:作为结构体的成员函数(常用),左操作数是 this,右操作数是参数
返回类型  类名::operator 运算符 (参数列表)const{   // const 代表操作不改变他原本的值
    对应运算符的操作
}

重载 < 运算符(最常用)

struct node{
    int a,b;
    int idx;
    bool f;
    bool operator<(const node& x) const {    // A& x 引用传参,避免拷贝,const代表只读
        if(a == x.a) {           // 末尾的const 表示不会修改this指向的对象
            return b > x.b;
        }
        return a < x.a;
    }
};

重载括号运算符

//结构体当作类,在c++中结构体默认是公有的
struct node{
    void operator()(int a,int b){
        return a>b;
    }
}
int main(){
    int a= 1,b=2;
    node();//是一个匿名对象 ,定义一个对象
    node val;
    val(2,2);    //调用重载函数
    //还可以 这样调用
    node()(a,b);    前面是一个匿名对象调用该重载函数
}

GCD–LCM

int gcd(int a,int b){
    return a%b?b%(a%b):b;
} //__gcd();

int lcm(int a,int b){
    return a/gcd(a,b)*b;    //原式 = a*b/gcd(a,b);  但是a*b可能出界
}

栈和队列

stack一种FILO(first in last out)(先进后出)的数据结构

#include<bits/stdc++.h>
#include<stack>
int main(){
    stack<数据类型> stk;
    stk.push(x);    //压入栈
    
    //必须保证栈非空,否则会报错(RE)runtime error
    stk.pop();     //将栈顶弹出
    stk.empty()    //判断栈是否为空,不为空返回true,否则返回false
    
    int T = stk.top();   //返回栈顶的元素,返回类型取决于定义类型 
    
    for(int i=1;i<=5;i++) stk.push(i);   
    while(!stk.empty()){   
        cout<<stk.top();    //将栈顶元素取出
        stk.pop();        //取出后,将栈顶元素出栈
    }    
    结果:54321
    return 0;
}

队列

1、普通队列

#include<bits/stdc++.h>
#include<queue>
int main(){
	queue<int> q;
    q.push(值)   //将值插入队列中
    q.pop()     //将队列中 队头元素出队(移除队头元素),并不输出
    q.front()   //返回队头元素,但不移除
    q.back()    //返回队尾元素,不移除
    q.empty()   //判断队列是否为空,不为空返回true,否则返回false
    for(int i=1;i<=5;i++){
		q.push(i);
    }    
    cout<<q.front();
    q.pop();
    cout<<q.front();
	//输出:12    
        
    ***
    //用队列将2个以上元素打包为一个元素
    queue< pair<int,int> > q; 或者 typedef pair<int,int> P; queue<P> q;
    //x = q.front().first   存储坐标x
    //y = q.front().second  存储坐标y
    ***
        
    将元素存入队列
    q.push(make_pair(x,y)); 或者  q.push({x,y});
}

2、双端队列 deque

介绍:给定一组序列,$[\;a_1,a_2,a_3 \cdots a_n\;]$ ,对于双端队列,可以分别在队头、队尾进行插入和删除操作

  • 输入受限:限制单侧插入,不限制删除操作(两端都可以删除)
  • 输出受限:限制单侧删除,不限制插入操作(两端都可以插入)

应用:模拟栈、滑动窗口、撤销/重做、任务调度

image-20250808201507569

// queue的底层容器是deque,但可以更改为list(queue也可以使用list)
queue<int,deque<int>> q;  <==> queue<int,list<int>> q; 
//deque操作
deque<int> dq;
1、dq.push_front(值);    //  首插
2、dq.push_back(值);     // 尾插
3、dq.pop_front();      // 删除队头元素
4、dq.pop_back();       // 删除队尾元素
dq.front();         //获取队头元素
dq.back();         //获取队尾元素
dq.size();     //计算元素个数
dq.empty();    //判空

3、优先队列 priority_queue

介绍:优先队列本质是用数组实现的堆(完全二叉树),可以拿出优先级最大的元素

大顶堆:值越大的元素优先级越高

小顶堆:值越小的元素优先级越高

priority_queue<int> que;  //默认大顶堆
que.size();
que.push(x);   //插入x
que.pop();     //删除优先级最高的元素

que.top();     //访问优先级最高的元素,堆顶元素
que.empty();  //判空
//第一个参数表示存储的参数类型
//第二个参数表示所用的容器
//第三个参数是(仿函数) 比较方法  less<int> 大顶堆    greater<int> 小顶堆
priority_queue<int,vector<int>, greater<int>> que;
struct node{
    int x,y;
    bool operator < (const node &b) const{
        return this->x < b.x;    //重载小于号less,大的数还是优先级高;若重载大于号,小的数优先级高
    }
};
priority_queue<node> que;
que1.push({1,2});
que1.push({9,-1});

while(!que.empty()){
	cout<< que.top().x << endl;
    que.pop();
}

集合 set

底层结构红黑树,有序,无重复,不支持随机访问[],插入删除复杂度 $O(logn)$

#include<set>
set<int> p;
p.empty();
p.size();
p.insert(value);
p.erase(value);

multiset

基于红黑树实现,有序,有重复,插入删除复杂度 $O(logn)$

#include<set>
multiset<int,greater<int>> ms; // 指定降序
ms.insert(x);    // 插入
ms.erase(x);     // 删除
// 遍历,访问,不允许 ms[i] 取值
for(auto i = ms.begin(); i != ms.end(); ++i){
    cout<< i << " ";      // 迭代器实现访问
}
for(auto& i : ms){  cout<< ms << " ";}
//二分查找
ms.lower_bound(v);  //第一个 >= 元素的迭代器
ms.upper_bound(v);  // 第一个 > 元素的迭代器
// 只有当 v 大于容器中所有元素时,返回 end() 迭代器

键值对 map

#include<bits/stdc++.h>  || #include<map>
map<T_key,T_value> m;
// map : 基于红黑树实现,按键排序,查找效率平均O(logn)
// 自定义类型需要比较时,重载小于号
struct node{
    int x,;
    bool operator < (const node &u) const{
        return x == u.x ? y < u.y : x < u.x;
    }
}
map<node,int> mp;
//常用操作
insert({key,value})
erase(key)
find(key)
operator[key] //返回指定的键值
count(key)   // mp.count(key) 访问该键所映射值的次数   
size()    //键值对数
clear()  empty()  begin() end()
//查找key是否存在
map<int,int> mp;
if(mp.find(key) != mp.end()) cout<<mp[key]<<'\n';
if(mp.count(key))  cout<<mp[key]<<'\n';

//遍历map
for(auto &i : mp) {
    cout<< i.first << " " << i.second << '\n';
}
for(auto &[key,val] : mp) {
    cout<< key << " " << val <<'\n';
}

unordered_map

// unordered_map 基于哈希表实现的map集合容器
// 元素无序,查找性能平均O(1),应用于需要快速查找,无序
// 头文件 #include<unordered_map>
unordred_map<string,int> mp;
mp.insert({"Alice",25});   // 增加键值对
mp.emplace("David",32);    //增加,效率更高
mp["Char"]=28;       // 若存在键修改,不存在插入

// 删除
mp.erase("Alice");
// 遍历
for(auto it = mp.begin(); it != mp.end(); ++it){
	cout<<it->first<<" "<< it->second <<endl;
}
mp.empty()     // 判空
mp.size()     // 键的数量

元组 tuple

若元素数超出2个,不想写结构体,可以使用

using node = tuple<ll, int, int>;
 // 建图时
vector<node> e;
// 取值
node a;
get<0>(a);  // 取第1个元素
// 对 tuple 排序,lambda表达式
sort(e.begin(),e.end(),
    [](const node& x,const node& y){
		return get<0>(x) < get(0).y;   // <第n个元素>从0索引开始            
    }
);

next_permutatiom

全排列

与其相对应的 prev_permutation

内容介绍:每运行一次就可以排列出下一次的全排列

语法:next_permutatiom(数组开头,数组结尾) 类似于sort

//用法
#include<algorithm>
#include<bits/stdc++.h>
using namespace std;
int a[1000];
int n;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
    //令a[i] = i;
    int tot = n;   //所有全排列的数量
    
    do{
    	for(int i=1;i<=tot;i++){
    		cout<<a[i]<<" ";
		}
		cout<<endl;
	}while(next_permutation(a+1,a+n+1)); 
    return 0;
}