上个月帮同事解决了一个相关问题,发现这个知识点很多人都不太清楚,特此整理成文章分享。
数组
数组的查找
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
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) 返回 0,if 条件判断为假,程序会继续执行后续代码,通过递归调用 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;
}}
浙公网安备 33010602011771号