上个月帮同事解决了一个相关问题,发现这个知识点很多人都不太清楚,特此整理成文章分享。

数组

数组的查找

int find(int arr[], int lenth, int key) {
	for (int i = 0; i < lenth; i++) {
		if (arr[i] == key) {
			return i;
		}
	}
	return -1;
}

数组插入

# include < iostream >
using namespace std;
#define MAXUM 6//没有分号
int main() {
	int arr [MAXUM] = { 1,23,4,6,8 };
	int lenth = 5;
	int element = 52;//要插入的数
	int pos = 2;//要插入的下标
	for (int i = lenth; i > pos; i--) {
		arr[i] = arr[i-1];//向右移
	}
	arr[pos] = element;
	lenth += 1;
	return 0;
}

数组删除

#include
#include
using namespace std;
int del(int arr[], int& length, int key)
 {//删除key
	auto it = find(arr,arr+length,key);
	if (it == arr+length)return -1;//未找到
	int index = distance(arr, it);
	for (int i = index; i < length-1; i++) {
		arr[i] = arr[i + 1];
	}
	length--;
	return index;
}

链表

定义一个链表

struct node{
int data;
struct node*next;
}
node* head=new node;//用new开辟一片内存
node* newhead=new node;
head->data=10;
head->next=nullptr;
newhead=head;//此时newhead的地址等于head,内部的属性也拷贝了head中的值
在指针中最后一个元素的指针指向null
struct LNode {
	int data;
	struct LNode* next;
};
int main() {
	//通过指针可以动态分配内存(如new或malloc),适用于数据大小未知或运行时才确定的情况(如链表、树)。
	LNode* head = new LNode;//动态分配一个节点
	LNode* middle = new LNode;
	LNode* last = new LNode;
	//传递指针可以减少内存开销
	head->data = 10;//直接修改对象
	middle->data = 20;
	last->data = 30;
	//链表、树、图等结构的节点间需要通过指针连接。
	head->next = middle;
	middle->next = last;
	last->next = nullptr;
struct node*tmp=head;
while (tmp != nullptr) {
	printf("%d", tmp->data);
	tmp = tmp->next;
}
}

-> 是 指针成员访问运算符,用于通过指针访问结构体或类的成员。
与.的区别:

.:直接访问对象成员(对象是变量,非指针)。
LNode node;
node.data = 10; // 使用`.`访问成员
->:通过指针访问对象成员(对象是指针)。
LNode* ptr = &node;
ptr->data = 20; // 等价于 (*ptr).data = 20

头插和尾插法

头插示意图

struct node {
	int data;
	struct node* next;
};
struct node* head ;
void headinsert(int e) {//链表头插法
	struct node* newhead = new node;
	newhead->data = e;
	newhead->next = head;
	head = newhead;
}
void tailinsert(int e) {//链表尾插法
	struct node* newin = new node;
	newin->data = e;
	newin->next = nullptr;
	if (head==nullptr) {//主要是对第一个链表使用,使其成为头部
		head = newin;
	}
	else {
		struct node* lastnode = head;
		while (lastnode->next != nullptr) {//判断是否为中间元素
			lastnode = lastnode->next;//如果是中间即非尾部就向后移
		}
		lastnode->next = newin;//直到找到尾部然后此时指针位于倒数第二个节点,让倒数第二个节点指向尾插的元素
	}

删除链表节点

bool deletnode(struct node** head, int key) {//这里是**的原因是要修改指针本身的值,例如删除开头,修改head内部的值,此时就要传递指针的地址了
	if (*head == nullptr)return false;
	node* tmp;
	if ((*head)->data == key) {
		tmp = *head;//因为head是**型,要先解引用才能得到原始的node*指针
		*head = (*head)->next;
		delete (tmp);
		return true;
	}
	else {
		node* pre = *head;
		while (pre->next != nullptr) {//如果到了尾部还没有找到就返回空
			if (pre->next->data == key) {
				tmp = pre->next;
				pre->next = pre->next->next;
				delete (tmp);
				return true;
			}
			else {
				pre = pre->next;
			}
		}
		return false;//如果整个循环都结束了还没有就返回null
	}
}

查找链表

node* findnode(node* head, int key) {
	node* tmp=head;
	while (tmp!= nullptr) {
		if (tmp->data==key) {
		return tmp;
		}else {
			tmp = tmp->next;
		}
	}
	return nullptr;
}

双向链表

struct node {//双向链表
	int data;
	struct node* next;
	struct node* piro;
};
int main() {
	node* head = new node;
	node* middle = new node;
	node* last = new node;
	head->data = 10;
	middle->data = 20;
	last->data = 30;
	head->piro = nullptr;
	middle->piro = head;
	last->piro = middle;
	head->next = middle;
	middle->next = last;
	last->next = nullptr;
	node* tmp = head;
	while (tmp != nullptr) {//头遍历
		printf("%d\n", tmp->data);
		tmp = tmp->next;
	}
	node* tail = last;
	while (tail != nullptr) {//尾遍历
		printf("%d\n", tail->data);
		tail = tail->piro;
	}
}

双链表头插法

#include using namespace std;
struct node {
    int data;
    node* next;
    node* prev;  // 将 piro 改为更直观的 prev};
// 创建哑节点(头节点)
node* create_head() {
    node* head = new node;
    head->data = 0;      // 哑节点 data 无意义,可设为 0
    head->prev = nullptr;
    head->next = nullptr;
    return head;}
bool insert(node* phead, int e) {  // 参数改为指针类型
    node* newnode = new node;
    newnode->data = e;
    // 新节点的前后指针设置
    newnode->next = phead->next;
    newnode->prev = phead;
    // 处理原第一个节点的 prev 指针
    if (phead->next != nullptr) {
        phead->next->prev = newnode;
    }
    // 更新头节点的 next 指针
    phead->next = newnode;
    return true;}
// 打印链表(验证用)void print_list(node* head) {
    node* current = head->next;
    while (current != nullptr) {
        cout << current->data << " ";
        current = current->next;
    }
    cout << endl;}
int main() {
    node* head = create_head();
    insert(head, 1);
    insert(head, 2);
    insert(head, 3);
    print_list(head); // 输出 3 2 1
    return 0;}

一个图由顶点和边组成,图分为有向图和无向图。

图像来源于图码

在无向图中(A,B)=(B,A),在有向图中<A,B>!=<B,A>

:在无向图中度(TD)代表一个顶点的边数,有向图则分入度(ID)和出度(OD),入度就是指向自己的边数,出度就是指出的边,TD=OD。

路径:从一个顶点到另一个顶点叫路径,有向图的路径方向必须和边的方向一致

回路、环:始点和终点为同一个点,就是回路或环

连通:在无向图中一个顶点到另一个顶点之间存在路径就是连通的;对有向图来说要探讨强连通性,强连通就是两个顶点之间存在方向相反的两条路径(双车道)。连通图是指无向图,强连通是指有向图。

无向图最少需要n-1条边才能形成连通图,多一条就会形成环;有向图最少需要n条边才能形成强连通图,同时也形成了一个回路

(强)连通分量:如大陆和台湾的高速网可以看成两个连通分量。无向图中的极大连通子图成为连通分量;有向图中的极大强连通子图成为有向图的强连通分量

极大(强)连通子图:包含尽可能多的顶点和边

权值:边上可以有权值,这种图叫带权图

红黑树与哈希表

一、红黑树(Red-Black Tree)

1. 基本概念

定义:红黑树是一种自平衡的二叉搜索树(BST),通过颜色标记和旋转操作维护平衡性。

核心性质

1.每个节点为红色或黑色。

2.根节点和叶子节点(NIL节点)必须为黑色。

3.红色节点的子节点必须为黑色(不能有连续的红色节点)。

4.从根到任意叶子节点的路径上,黑色节点的数量相同(黑高一致)。

2. 平衡机制

旋转操作:通过左旋和右旋调整树的结构。

颜色翻转:插入或删除时,通过调整节点颜色和旋转操作修复平衡性。

时间复杂度:插入、删除、查找均为 O(log n)。

3. 优点

严格有序:所有键按顺序排列,支持范围查询(如遍历区间 [a, b])。

性能稳定:操作时间始终为 O(log n),无最坏情况波动。

无需哈希函数:适用于无法高效哈希的键类型(如自定义对象)。

4. 缺点

内存开销:每个节点需存储颜色、父指针、左右子指针等信息。

实现复杂:需处理多种平衡情况(插入后的修复可能涉及多次旋转)。

5. 典型应用

C++ std::map 和 std::set。

Java TreeMap。

数据库索引(如B树变种)


二、哈希表(Hash Table)

1. 基本概念

定义:通过哈希函数将键映射到数组(桶)的索引位置,解决冲突后存储键值对。

核心组件:

哈希函数:将键转换为整数(决定分布均匀性)。

冲突解决策略:

链地址法:每个桶存储链表或红黑树(如Java HashMap)。

开放寻址法:线性探测、二次探测等。

2. 性能特性

理想时间复杂度:插入、删除、查找为 平均 O(1)。

最坏情况:哈希冲突严重时退化为 O(n)(如所有键映射到同一桶)。

3. 扩容机制

负载因子:当元素数量与桶数量的比值超过阈值时(如0.75),触发扩容(rehashing)。

动态扩容:新建更大的桶数组,重新哈希所有键值对(时间复杂度 O(n))。

4. 优点

高效查找:平均 O(1) 的时间复杂度优于树结构。

内存紧凑:数组结构缓存友好,内存利用率高。

5. 缺点

无序性:无法直接支持范围查询或有序遍历。

依赖哈希函数:键类型需支持高效哈希和相等比较。

性能波动:哈希冲突可能导致性能下降。

6. 典型应用

C++ std::unordered_map 和 std::unordered_set。

Java HashMap 和 HashSet。

缓存系统(如Redis哈希表)。


三、红黑树 vs 哈希表

特性

红黑树

哈希表

底层结构

自平衡二叉搜索树

数组 + 链表/红黑树

⚠️ 踩坑记录:

我在实际项目中遇到过一个问题,这个配置在开发环境正常,但生产环境会报错。 后来发现是因为生产环境的版本不一致导致的。建议大家在部署前一定要检查版本兼容性。

元素顺序

有序

无序

插入/查找平均时间

O(log n)

O(1)(理想情况)

最坏时间

O(log n)

O(n)(哈希冲突严重时)

内存占用

较高(节点存储额外信息)

较低(数组紧凑)

键类型要求

需支持 < 或自定义比较规则

需支持哈希函数和 == 比较

适用场景

有序数据、范围查询

快速查找、无需顺序


四、如何选择?

选择红黑树的情况:

需要元素按顺序排列(如遍历、范围查询)。

键类型无法高效哈希(如自定义对象)。

需要稳定的性能(避免哈希冲突的不确定性)。

选择哈希表的情况:

追求极致的查找、插入速度(平均 O(1))。

无需维护元素顺序。

键类型有高质量的哈希函数(如整数、字符串)。


五、代码示例

C++ 中的红黑树(std::map)

#include 
std::map rbTree;
rbTree[3] = "Three";  // 插入
rbTree.find(3);        // 查找

C++ 中的哈希表(std::unordered_map)

#include 
std::unordered_map hashTable;
hashTable[3] = "Three";  // 插入
hashTable.find(3);       // 查找

六、总结

红黑树:牺牲部分时间性能换取有序性和稳定性,适合需要顺序操作的场景。

哈希表:以空间换时间,提供高效的随机访问,适合快速查找但无需顺序的场景。
根据需求选择合适的数据结构,是优化程序性能的关键!

二叉树

前序遍历:根左右

中序遍历:左根右

后序遍历:左右根

完全二叉树:除叶子结点以外全都填满,且叶子结点要尽量靠左,否则不是一个完全二叉树

i>1时,结点i的双亲下标为i/2,向下取整
2*i<=n时,结点i的左孩子下标为2i,否则无左孩子
2*i+1<=n时,结点i的右孩子下标为2i+1,否则无右孩子

建议完全二叉树和满二叉树使用数组存储,因为数组是按照完全二叉树的关系划分的,如果不是上述两种树,则会浪费空余区域

因为假如上面二叉树没有DEF

那么数组应该为/ABC///G

一般二叉树采用链式结构存储

DFS、BFS

一、vector 在 DFS 中的核心作用

1. 动态路径记录

场景:全排列、组合问题中记录当前选择的路径。
特点:动态增删元素,支持回溯操作。
代码示例:全排列问题

vector> permute(vector& nums) {//定义一个返回值为二维数组的函数,参数为一个vector(向量)
    vector> res;//用res存储结果
    vector path;          // 当前路径
    vector used(nums.size(), false); // 标记已选元素
    function dfs = [&]() {
        if (path.size() == nums.size()) {
            res.push_back(path); // 保存完整路径
            return;
        }
        for (int i = 0; i < nums.size(); ++i) {
            if (used[i]) continue;
            used[i] = true;
            path.push_back(nums[i]); // 选择元素
            dfs();
            path.pop_back();        // 回溯(删除最后一个元素)
            used[i] = false;
        }
    };
    dfs();
    return res;}
  • function是 C++ 标准库 <functional> 头文件中定义的一个通用的多态函数包装器类型。它可以存储、复制和调用任何可调用对象,如函数、函数指针、成员函数指针、Lambda 表达式等。
  • <void()>是 std::function 的模板参数列表,用于指定该可调用对象的签名。void 表示返回值类型为 void,即不返回任何值;() 表示该可调用对象不接受任何参数。

2. 多维状态管理

场景:网格类问题(如岛屿数量)中记录访问状态。
特点:直接修改原数组或使用二维 vector。
代码示例:岛屿数量

void dfs(vector>& grid, int x, int y) {
    if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size() || grid[x][y] != '1')
        return;
    grid[x][y] = '0'; // 直接修改原数组标记已访问
    dfs(grid, x+1, y);
    dfs(grid, x-1, y);
    dfs(grid, x, y+1);
    dfs(grid, x, y-1);}

使用引用而不是传参是因为引用直接传本体,而非复制一份备份,内存下降。

而且使用引用可以修改原函数值

二、unordered_map 在 DFS 中的高级应用

1. 记忆化搜索(Memoization)

场景:避免重复计算子问题(如斐波那契数列、图遍历中的重复路径)。
特点:以状态参数为键,缓存计算结果。
代码示例:斐波那契数列

unordered_map memo; // 缓存已计算的结果
int fibonacci(int n) {
    if (n <= 1) return n;
    if (memo.count(n)) return memo[n]; // 命中缓存
    memo[n] = fibonacci(n-1) + fibonacci(n-2); // 保存结果
    return memo[n];}

memo 用于缓存斐波那契数列中已经计算过的结果,其中键 int 代表斐波那契数列的项数,值 int 代表对应项数的斐波那契数。

unordered_map 的 count 方法用于检查容器中是否存在指定的键。其函数原型如下:

size_type count(const Key& key )const;

参数key 是要查找的键。

返回值:如果容器中存在该键,返回 1;若不存在,则返回 0。这是因为 unordered_map 里每个键都是唯一的,所以返回值只能是 0 或者 1

代码中缓存命中的原理

在 fibonacci 函数里,if (memo.count(n)) return memo[n]; 这行代码的作用是检查 memo 中是否已经计算过第 n 项的斐波那契数。具体步骤如下:

调用 memo.count(n):该函数会检查 memo 中是否存在键为 n 的元素。

若存在memo.count(n) 返回 1,此时 if 条件判断为真,意味着已经计算过第 n 项的斐波那契数,也就是缓存命中。接着通过 memo[n] 直接获取该键对应的值,也就是第 n 项的斐波那契数,然后将其返回,避免了重复计算。

若不存在memo.count(n) 返回 0if 条件判断为假,程序会继续执行后续代码,通过递归调用 fibonacci(n - 1) 和 fibonacci(n - 2) 来计算第 n 项的斐波那契数,然后把计算结果存储到 memo[n] 中,方便后续使用。

3. 快速查找与去重

场景:需要快速判断元素是否存在(如单词接龙中的已访问单词)。
特点:O(1) 时间复杂度的查找。
代码示例:单词接龙

unordered_set dict(wordList.begin(), wordList.end()); // 字典快速查找
void dfs(string current, unordered_set& visited, vector& path) {
    if (current == endWord) {
        res.push_back(path);
        return;
    }
    for (int i = 0; i < current.size(); ++i) {
        char original = current[i];
        for (char c = 'a'; c <= 'z'; ++c) {
            current[i] = c;
            if (dict.count(current) && !visited.count(current)) { // O(1) 查找
                visited.insert(current); // 标记已访问
                path.push_back(current);
                dfs(current, visited, path);
                path.pop_back();
                visited.erase(current); // 回溯
            }
        }
        current[i] = original;
    }}