前言
C++数据结构与算法
学习算法参考:https://www.hello-algo.com/
Visual Studio快捷键
https://learn.microsoft.com/zh-cn/visualstudio/ide/default-keyboard-shortcuts-in-visual-studio?view=vs-2019
启动时不调试 Ctrl+F5
设置文档格式 Ctrl+K、Ctrl+D
注释选定内容 Ctrl+K、Ctrl+C
取消注释选定内容 Ctrl+K、Ctrl+U
行 - 删除 Ctrl+Shift+L
复制行 Ctrl+D
MarkDown公式编辑总结
https://zhuanlan.zhihu.com/p/357093758
变量和常量命名规范
- 类名:首字母大写和驼峰原则
- 方法名(函数)、类成员变量、局部变量、package包命名:首字母小写和驼峰原则
- 常量:大写字母和下划线
时间复杂度
设输入数据大小为n ,常见的时间复杂度类型(按照从低到高的顺序排列)。
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( 2 n ) < O ( n ! ) 常数阶 < 对数阶 < 线性阶 < 线性对数阶 < 平方阶 < 指数阶 < 阶乘阶 O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(2^n)<O(n!)\\ 常数阶<对数阶<线性阶<线性对数阶<平方阶<指数阶<阶乘阶 O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)<O(n!)常数阶<对数阶<线性阶<线性对数阶<平方阶<指数阶<阶乘阶
空间复杂度
设输入数据大小为 n ,常见的空间复杂度类型(按照从低到高的顺序排列)。
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
2
)
<
O
(
2
n
)
常数阶
<
对数阶
<
线性阶
<
平方阶
<
指数阶
O(1)<O(logn)<O(n)<O(n^2)<O(2^n)\\ 常数阶<对数阶<线性阶<平方阶<指数阶
O(1)<O(logn)<O(n)<O(n2)<O(2n)常数阶<对数阶<线性阶<平方阶<指数阶
vector
#include<bits/stdc++.h>
using namespace std;
int main() {
//初始化
vector<int>vec = { 1,2 };
//构建一个5×5,且所有元素均为0的二维动态数组
vector<vector<int>> arrVec(5, vector<int>(5, 0));
/* 访问元素 */
int num = vec[1]; // 访问索引 1 处的元素
/* 更新元素 */
vec[1] = 0; // 将索引 1 处的元素更新为 0
/*清空列表*/
vec.clear();
/* 尾部添加元素 */
vec.push_back(1);
vec.push_back(3);
//加到尾部
vec.emplace_back(4);
//插入元素,插入头部
vec.insert(vec.begin(), 11);
/* 中间插入元素 */
vec.insert(vec.begin() + 3, 6); // 在索引 3 处插入数字 6
/* 删除元素 */
vec.erase(vec.begin() + 3); // 删除索引 3 处的元素
/* 拼接两个列表 */
vector<int> vec2 = { 6, 8, 7, 10, 9 };
// 将vec 后面添加 vec2
vec.insert(vec.end(), vec2.begin(), vec2.end());
/* 排序列表 */
sort(vec.begin(), vec.end()); // 排序后,默认元素从小到大排列
//从大到小排序
sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b;
});
//遍历
for (int v : vec) cout << v << " ";
return 0;
}
链表
单链表
#include<bits/stdc++.h>
using namespace std;
/***
*
*单链表
*
***/
//链表数据结构
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}; //构造方法
};
//遍历链表
void printListNode(ListNode* node) {
ListNode* root = node;
while (root != nullptr) {
cout << root->val << " ";
root = root->next;
}
cout << endl;
}
//增加节点
void addListNode(ListNode* node, ListNode* newNode) {
cout << "add num = " << newNode->val << endl;
newNode->next = node->next;
node->next = newNode;
}
//删除节点
void deleListNode(ListNode* node) {
//只有一个元素的时候
if (node->next == nullptr) return;
ListNode* delnode = node->next;
cout << "dele num = " << delnode->val << endl;
node->next = delnode->next;
delete delnode; //删除这个节点的内存,在 C 和 C++ 等语言中,需要手动释放节点内存。
}
//修改节点
void updateListNode(ListNode* node, int updateNum) {
//只有一个元素的时候
if (node->next == nullptr) return;
ListNode* updateNode = node->next;
cout << "original num = " << updateNode->val << " to update num = " << updateNum << endl;
updateNode->val = updateNum;
}
//访问某一节点
void lookListNode(ListNode* node, int index) {
ListNode* root = node;
int idx = index;
while (root != nullptr && idx > 0) {
root = root->next;
--idx;
}
int num = root->val;
printListNode(node);
cout << "index = " << index << ",val = " << num << endl;
}
//查找某个值所在的节点和索引值
void findListNode(ListNode* node, int target) {
ListNode* root = node;
int idx = 0;
while (root != nullptr) {
if (root->val == target) {
cout << "idx = " << idx << ",target = " << target << ",after the node is: ";
printListNode(root);
break;
}
root = root->next;
++idx;
}
}
int main() {
/* 初始化链表 2 -> 5 -> 3 -> 8 */
ListNode* n0 = new ListNode(2);
ListNode* n1 = new ListNode(5);
ListNode* n2 = new ListNode(3);
ListNode* n3 = new ListNode(8);
n0->next = n1;
n1->next = n2;
n2->next = n3;
cout << "[+] original node:" << endl;
printListNode(n0);
cout << "[+] add node:" << endl;
ListNode* newNode = new ListNode(7);
addListNode(n0, newNode);
printListNode(n0);
cout << "[+] dele node:" << endl;
deleListNode(n0);
printListNode(n0);
cout << "[+] update node with num:" << endl;
updateListNode(n0, 9);
printListNode(n0);
cout << "[+] look node with index:" << endl;
lookListNode(n0, 2);
cout << "[+] find node with target:" << endl;
printListNode(n0);
findListNode(n0, 9);
return 0;
}
Stack栈
「栈 stack」是一种遵循先进后出的逻辑的线性数据结构。栈的底层由链表实现。
#include<bits/stdc++.h>
using namespace std;
int main() {
/* 初始化栈 */
stack<int> stack;
/* 元素入栈 */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(6);
/* 访问栈顶元素 */
int top = stack.top();
cout << "top num = " << top << endl;
/* 元素出栈 */
stack.pop(); // 无返回值,把栈顶的值删除
/* 获取栈的长度 */
int size = stack.size();
cout << "size = " << size << endl;
/* 判断是否为空 */
bool empty = stack.empty(); //非空返回false
cout << "empty = " << empty << endl;
return 0;
}
基于链表实现的栈
#include<bits/stdc++.h>
using namespace std;
/* 基于链表实现的栈 */
class LinkedListStack {
private:
//链表数据结构
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}; //构造方法
};
ListNode* stackTop; // 将头节点作为栈顶
int stkSize; // 栈的长度
public:
// LinkedListStack类的构造方法
LinkedListStack() {
cout << "启动了 LinkedListStack的构造方法" << endl;
stackTop = nullptr;
stkSize = 0;
}
/**
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。
析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。
**/
~LinkedListStack() {
// 遍历链表删除节点,释放内存
freeMemoryLinkedList(stackTop);
}
void freeMemoryLinkedList(ListNode* stackTop) {
cout << "调用了 LinkedListStack 析构函数" << endl;
delete stackTop;
};
/* 获取栈的长度 */
int size() {
return stkSize;
}
/* 判断栈是否为空 */
bool isEmpty() {
return size() == 0;
}
/* 入栈 */
void push(int num) {
ListNode* node = new ListNode(num);
//新的值查到头部(栈顶)
node->next = stackTop;
//stackTop变成头节点
stackTop = node;
stkSize++;
}
/* 出栈 */
void pop() {
int num = top();
ListNode* tmp = stackTop;
stackTop = stackTop->next;
// 释放内存
delete tmp;
stkSize--;
}
/* 访问栈顶元素 */
int top() {
if (isEmpty())
throw out_of_range("Stack is null");
return stackTop->val;
}
/* 将 List 转化为 Array 并返回 */
vector<int> toVector() {
ListNode* node = stackTop;
vector<int> res(size());
for (int i = res.size() - 1; i >= 0; i--) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
int main() {
LinkedListStack listStack;
// 栈顶到栈底顺序: 8 ->5 ->2 ->3 ->1
listStack.push(1);
listStack.push(3);
listStack.push(2);
listStack.push(5);
listStack.push(8);
vector<int> arr = listStack.toVector();
for (int v : arr) cout << v << " ";
cout << endl;
listStack.pop();
cout << "after pop:" << endl;
vector<int> arr2 = listStack.toVector();
for (int v2 : arr2) cout << v2 << " ";
cout << endl;
int topNum = listStack.top();
cout << "topNum = " << topNum << " Stack size = " << listStack.size() << endl;
return 0;
}
Queue队列
「队列 queue」是一种遵循先进先出规则的线性数据结构。队列的底层由链表实现。
#include<bits/stdc++.h>
using namespace std;
int main() {
/* 初始化队列 */
queue<int> queue;
/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 访问队首元素 */
int front = queue.front();
/* 访问队尾元素 */
int back = queue.back();
cout << "front = " << front << " back = " << back << endl;
/* 元素出队,会删除元素 */
queue.pop();
/* 获取队列的长度 */
int size = queue.size();
/* 判断队列是否为空 */
bool empty = queue.empty(); //非空返回false
cout << "size = " << size << " empty = " << empty << endl;
return 0;
}
基于链表实现的队列
链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。
#include<bits/stdc++.h>
using namespace std;
/* 基于链表实现的队列 */
class LinkedListQueue {
private:
//链表数据结构
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}; //构造方法
};
ListNode* front, * rear; // 头节点 front ,尾节点 rear
int queSize;
public:
LinkedListQueue() {
cout << "启动了 LinkedListQueue的构造方法" << endl;
front = nullptr;
rear = nullptr;
queSize = 0;
}
~LinkedListQueue() {
// 遍历链表删除节点,释放内存
freeMemoryLinkedList(front);
}
void freeMemoryLinkedList(ListNode* front) {
cout << "调用了 LinkedListQueue 析构函数" << endl;
delete front;
}
/* 获取队列的长度 */
int size() {
return queSize;
}
/* 判断队列是否为空 */
bool isEmpty() {
return queSize == 0;
}
/* 尾部节点入队 */
void push(int num) {
// 尾节点后添加 num
ListNode* node = new ListNode(num);
// 如果队列为空,则令头、尾节点都指向该节点
if (front == nullptr) {
front = node;
rear = node;
}
// 如果队列不为空,则将该节点添加到尾节点后
else {
rear->next = node;
rear = node;
}
queSize++;
}
/* 头部节点出队 */
void pop() {
// 删除头节点
ListNode* tmp = front;
front = front->next;
// 释放内存
delete tmp;
queSize--;
}
/* 访问队首元素 */
int peek() {
if (size() == 0)
throw out_of_range("Queue is null");
return front->val;
}
/* 将链表转化为 Vector 并返回 */
vector<int> toVector() {
ListNode* node = front;
vector<int> res(size());
for (int i = 0; i < res.size(); i++) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
int main() {
LinkedListQueue queue;
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(4);
queue.push(8);
queue.push(6);
vector<int> arr = queue.toVector();
for (int v : arr) cout << v << " ";
cout << endl;
queue.pop();
cout << "after pop:" << endl;
vector<int> arr2 = queue.toVector();
for (int v2 : arr2) cout << v2 << " ";
cout << endl;
int topNum = queue.peek();
cout << "peekNum = " << topNum << " Stack size = " << queue.size() << endl;
return 0;
}
double-ended queue双向队列
在头部和尾部都允许执行元素的添加或删除操作。
pushFirst() 将元素添加至队首
pushLast() 将元素添加至队尾
popFirst() 删除队首元素
popLast() 删除队尾元素
peekFirst() 访问队首元素
peekLast() 访问队尾元素
#include<bits/stdc++.h>
using namespace std;
int main() {
deque<int>deque;
//1->2->3->4
deque.push_back(3); // 添加至队尾
deque.push_back(4);
deque.push_front(2);// 添加至队首
deque.push_front(1);
/* 访问元素,不删除元素 */
int front = deque.front(); // 队首元素
int back = deque.back(); // 队尾元素
cout << "front = " << front << " back = " << back << endl;
/* 元素出队 ,删除元素*/
deque.pop_front(); // 队首元素出队
deque.pop_back(); // 队尾元素出队
/* 获取双向队列的长度 */
int size = deque.size();
/* 判断双向队列是否为空 */
bool empty = deque.empty();
cout << "size = " << size << " empty = " << empty << endl;
return 0;
}
基于双向链表实现双向队列
双向队列底层是由双向链表实现
#include<bits/stdc++.h>
using namespace std;
/* 基于双向链表实现的双向队列 */
class LinkedListDeque {
private:
/* 双向链表节点 */
struct DoublyListNode {
int val; // 节点值
DoublyListNode* next; // 后继节点指针
DoublyListNode* prev; // 前驱节点指针
DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {
}
};
DoublyListNode* front, * rear; // 头节点 front ,尾节点 rear
int queSize = 0; // 双向队列的长度
public:
/* 构造方法 */
LinkedListDeque() : front(nullptr), rear(nullptr) {
cout << "调用了 LinkedListDeque 的构造方法" << endl;
}
/* 析构方法 */
~LinkedListDeque() {
cout << "调用了 LinkedListDeque 的析构方法" << endl;
// 遍历链表删除全部节点,释放内存,
DoublyListNode* pre, * cur = front;
while (cur != nullptr) {
pre = cur;
cur = cur->next;
delete pre;
}
}
/* 获取双向队列的长度 */
int size() {
return queSize;
}
/* 判断双向队列是否为空 */
bool isEmpty() {
return size() == 0;
}
/* 入队操作 */
void push(int num, bool isFront) {
DoublyListNode* node = new DoublyListNode(num);
// 若链表为空,则令 front, rear 都指向 node
if (isEmpty())
front = rear = node;
// 队首入队操作
else if (isFront) {
// 将 node 添加至链表头部 (新增的node <-> 当前的front)
front->prev = node;
node->next = front;
front = node; // 更新头节点
// 队尾入队操作
}
else {
// 将 node 添加至链表尾部 (当前的rear <-> 新增的node )
rear->next = node;
node->prev = rear;
rear = node; // 更新尾节点
}
queSize++; // 更新队列长度
}
/* 队首入队 */
void pushFirst(int num) {
push(num, true);
}
/* 队尾入队 */
void pushLast(int num) {
push(num, false);
}
/* 出队操作 */
int pop(bool isFront) {
if (isEmpty())
throw out_of_range("Double queue is null!");
int val;
// 队首出队操作
if (isFront) {
val = front->val; // 暂存头节点值
// 删除头节点
DoublyListNode* fNext = front->next; //(第一个的头节点front <-> 下一个节点fNext)
if (fNext != nullptr) {
fNext->prev = nullptr;
front->next = nullptr;
delete front; //在 C 和 C++ 中需要手动释放内存。
}
front = fNext; // 更新头节点
// 队尾出队操作
}
else {
val = rear->val; // 暂存尾节点值
// 删除尾节点
DoublyListNode* rPrev = rear->prev; //(上一个节点rPrev <-> 最后的尾节点rear)
if (rPrev != nullptr) {
rPrev->next = nullptr;
rear->prev = nullptr;
delete rear; //在 C 和 C++ 中需要手动释放内存。
}
rear = rPrev; // 更新尾节点
}
queSize--; // 更新队列长度
return val;
}
/* 队首出队 */
int popFirst() {
return pop(true);
}
/* 队尾出队 */
int popLast() {
return pop(false);
}
/* 访问队首元素 */
int peekFirst() {
if (isEmpty())
throw out_of_range("Double queue is null!");
return front->val;
}
/* 访问队尾元素 */
int peekLast() {
if (isEmpty())
throw out_of_range("Double queue is null!");
return rear->val;
}
/* 返回数组用于打印 */
vector<int> toVector() {
DoublyListNode* node = front;
vector<int> res(size());
for (int i = 0; i < res.size(); i++) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
int main() {
LinkedListDeque doubleQueue;
// 0->1->2->3->4->5
doubleQueue.pushFirst(2); //队列头部添加元素
doubleQueue.pushFirst(1);
doubleQueue.pushFirst(0);
doubleQueue.pushLast(3); //队列尾部添加元素
doubleQueue.pushLast(4);
doubleQueue.pushLast(5);
vector<int>dqueue = doubleQueue.toVector();
cout << "dqueue num:" << endl;
for (int dq : dqueue) cout << dq << " ";
cout << endl;
doubleQueue.popFirst();//队列头部删除弹出元素
doubleQueue.popLast();//队列尾部删除弹出元素
cout << "after popFirst and popLast:" << endl;
int peekFirstNum = doubleQueue.peekFirst(); //访问头部元素,不删除
int peekLastNum = doubleQueue.peekLast(); //访问尾部元素,不删除
cout << "peekFirstNum = " << peekFirstNum << " peekLastNum = " << peekLastNum << endl;
int size = doubleQueue.size();
bool empty = doubleQueue.isEmpty(); //非空返回false
cout << "size = " << size << " empty = " << empty << endl;
return 0;
}
unordered_map哈希表
哈希表中进行增删查改的时间复杂度都是O(1)
#include<bits/stdc++.h>
using namespace std;
int main() {
/* 初始化哈希表 */
unordered_map<int, string> map;
/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map[1] = "小哈";
map[2] = "小啰";
map[3] = "小算";
map[4] = "小法";
map[5] = "小鸭";
/* 查询操作 */
// 向哈希表输入键 key ,得到值 value
string name = map[1];
/* 删除操作 */
// 在哈希表中删除键值对 (key, value),参数是key
map.erase(2);
//查找元素,参数是key,如果key不存在,find会返回end
if (map.find(3) != map.end()) cout << "find isExistKey true " << endl;
else cout << "find isExistKey false " << endl;
//查找元素,参数是key,存在返回1,不存在返回0
int isExistKey = map.count(5);
cout << "count isExistKey = " << isExistKey << " (存在返回1,不存在返回0)" << endl;
/* 遍历哈希表 */
// 遍历键值对 key->value
for (auto kv : map) {
cout << kv.first << " -> " << kv.second << endl;
}
//c++ 17特性
/*for (auto [k, v] : map) {
cout << "key = " << k << " value = " << v << endl;
}*/
//删除全部元素
map.clear();
//统计map的大小
int size = map.size();
cout << "after map clear,the map size = " << size << endl;
return 0;
}
哈希表的实现
输入一个 key
,哈希函数的计算过程分为以下两步。
-
- 通过某种哈希算法
hash()
计算得到哈希值。
- 通过某种哈希算法
-
- 将哈希值对桶数量(数组长度)
capacity
取模,从而获取该key
对应的数组索引index
。
- 将哈希值对桶数量(数组长度)
index = hash(key) % capacity
就可以利用 index
在哈希表中访问对应的桶,从而获取 value
。
#include<bits/stdc++.h>
using namespace std;
/* 键值对 */
struct Pair {
public:
int key;
string val;
Pair(int key, string val) {
this->key = key;
this->val = val;
}
};
/* 基于数组简易实现的哈希表 */
class ArrayHashMap {
private:
// [{key,val},{key,val},{key,val} ...]
vector<Pair*> buckets; //定义数组桶
int capacity = 100; // 数组桶大小
public:
ArrayHashMap() {
cout << "调用了 ArrayHashMap 的构造方法" << endl;
// 初始化数组,包含 100 个桶
buckets = vector<Pair*>(capacity);
}
~ArrayHashMap() {
cout << "调用了 ArrayHashMap 的析构方法" << endl;
// 释放内存
for (const auto& bucket : buckets) {
delete bucket;
}
buckets.clear();
}
/* 哈希函数 */
int hashFunc(int key) {
//对key值取模,如 4 % 100 = 4
int index = key % capacity;
return index;
}
/* 查询操作 */
string get(int key) {
int index = hashFunc(key);
Pair* pair = buckets[index];
if (pair == nullptr)
return nullptr;
//key通过hash后的index,拿到index对应的pair
return pair->val;
}
/* 添加操作 */
void put(int key, string val) {
//初始化pair对象
Pair* pair = new Pair(key, val);
int index = hashFunc(key);
//key通过hash后的index添加到桶里
buckets[index] = pair;
}
/* 删除操作 */
void remove(int key) {
int index = hashFunc(key);
// 先释放内存,在置为 nullptr
delete buckets[index];
buckets[index] = nullptr;
}
/* 获取所有键值对 */
vector<Pair*> pairSet() {
vector<Pair*> pairSet;
for (Pair* pair : buckets) {
if (pair != nullptr) {
pairSet.push_back(pair);
}
}
return pairSet;
}
/* 获取所有键 */
vector<int> keySet() {
vector<int> keySet;
for (Pair* pair : buckets) {
if (pair != nullptr) {
keySet.push_back(pair->key);
}
}
return keySet;
}
/* 获取所有值 */
vector<string> valueSet() {
vector<string> valueSet;
for (Pair* pair : buckets) {
if (pair != nullptr) {
valueSet.push_back(pair->val);
}
}
return valueSet;
}
/* 打印哈希表 */
void print() {
for (Pair* kv : pairSet()) {
cout << kv->key << " -> " << kv->val << endl;
}
}
};
int main() {
ArrayHashMap map;
map.put(0, "0");
map.put(1, "1");
map.put(2, "2");
map.put(3, "3");
map.put(4, "4");
int getKey = 2;
string val = map.get(getKey);
cout << "getKey = " << getKey << " value = " << val << endl;
int removeKey = 4;
map.remove(removeKey);
// 或者 map.print()
vector<Pair*> vps = map.pairSet();
for (Pair* vp : vps) {
cout << "key = " << vp->key << " value = " << vp->val << endl;
}
return 0;
}
哈希冲突
通常情况下哈希函数的输入空间远大于输出空间,改良哈希表数据结构,使得哈希表可以在存在哈希冲突时正常工作。即当哈希冲突比较严重时,才执行扩容操作。哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。
链式地址
在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。
- 查询元素:输入
key
,经过哈希函数得到桶索引,即可访问链表头节点,然后遍历链表并对比key
以查找目标键值对。 - 添加元素:先通过哈希函数访问链表头节点,然后将节点(即键值对)添加到链表中。
- 删除元素:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点,并将其删除。
#include<bits/stdc++.h>
using namespace std;
/* 键值对 */
struct Pair {
int key;
string val;
Pair(int key, string val) {
this->key = key;
this->val = val;
}
};
/* 链式地址哈希表 */
class HashMapChaining {
private:
int size; // 所有键值对的总和个数
int capacity; // 最外层的哈希表容量
double loadThres; // 触发扩容的负载因子阈值,这里设置成2/3
int extendRatio; // 扩容倍数。以下实现包含哈希表扩容方法。当负载因子超过2/3时,将哈希表扩容至2倍。
//[[{key,val},{key,val},{key,val}...],[{key,val},{key,val},{key,val}...],[{key,val},{key,val},{key,val}...]]
vector<vector<Pair*>> buckets; // 桶数组
public:
/* 构造方法 ,且初始化变量的值*/
HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {
cout << "调用了 HashMapChaining 的构造方法" << endl;
//重新定义桶数组buckets的大小
buckets.resize(capacity);
}
/* 析构方法 */
~HashMapChaining() {
cout << "调用了 HashMapChaining 的析构方法" << endl;
for (auto& bucket : buckets) {
for (Pair* pair : bucket) {
// 释放内存
delete pair;
}
}
}
/* 哈希函数 */
int hashFunc(int key) {
// key % 数组桶大小的容量
//index是buckets第一层数组的数据
int index = key % capacity;
return index;
}
/* 负载因子 */
double loadFactor() {
//所有键值对的大小容量跟最外层的桶容量的占比
return (double)size / (double)capacity;
}
/* 扩容哈希表(最外层) */
void extend() {
// 暂存原哈希表
vector<vector<Pair*>> bucketsTmp = buckets;
// 初始化扩容后的新哈希表,扩容至原来的2倍
capacity *= extendRatio;
//清空原来的桶数组的数据
buckets.clear();
//重新定义桶数组的大小
buckets.resize(capacity);
size = 0;
// 将键值对从原哈希表搬运至新哈希表
for (auto& bucket : bucketsTmp) {
for (Pair* pair : bucket) {
//重新存数据
put(pair->key, pair->val);
// 删除释放临时的内存
delete pair;
}
}
}
/* 添加操作 */
void put(int key, string val) {
// 当存储数据时,发现负载因子超过2/3阈值时,执行扩容
//也就是说所有键值对的大小容量跟最外层的桶容量的占比大于2/3时则扩容
if (loadFactor() > loadThres) {
extend();
}
int index = hashFunc(key);
// 遍历桶,若遇到指定 key存在时 ,则更新对应 val 并返回
for (Pair* pair : buckets[index]) {
if (pair->key == key) {
pair->val = val;
cout << "find the key data,key = " << key << ",update it!" << endl;
return;
}
}
// 若无该 key ,则将键值对添加至尾部
buckets[index].push_back(new Pair(key, val));
size++;
}
/* 查询操作 */
string get(int key) {
int index = hashFunc(key);
// 遍历桶,若找到 key 则返回对应 val
for (Pair* pair : buckets[index]) {
if (pair->key == key) {
return pair->val;
}
}
cout << "Not find the key = " << key << "data!" << endl;
// 若未找到 key 则返回 nullptr
return nullptr;
}
/* 删除操作 */
void remove(int key) {
int index = hashFunc(key);
vector<Pair*>& bucket = buckets[index];
// 遍历桶,从中删除键值对
for (int i = 0; i < bucket.size(); i++) {
if (bucket[i]->key == key) {
Pair* tmp = bucket[i];
bucket.erase(bucket.begin() + i); // 从中删除键值对
delete tmp; // 释放内存
size--;
return;
}
}
}
//返回整个外层桶数组的大小,也就是说是capacity的大小
int mapSize() {
return capacity;
}
//返回相同key的桶数组大小
int sizeWithKey(int key) {
int index = hashFunc(key);
vector<Pair*>bucket = buckets[index];
return bucket.size();
}
/* 打印哈希表 */
void print() {
for (vector<Pair*>& bucket : buckets) {
cout << "[";
for (Pair* pair : bucket) {
cout << pair->key << " -> " << pair->val << ", ";
}
cout << "]\n";
}
}
};
int main() {
HashMapChaining mapChain;
mapChain.put(0, "0");
mapChain.put(1, "1");
int Key1 = 1;
string val = mapChain.get(Key1);
cout << "Key1 = " << Key1 << ",val = " << val << endl;
mapChain.print();
int capacity = mapChain.mapSize();
cout << "capacity = " << capacity << endl;
mapChain.put(5, "5");
mapChain.put(3, "3");
mapChain.put(4, "4");
mapChain.print();
int capacity2 = mapChain.mapSize();
cout << "after put data ,the capacity2 = " << capacity2 << endl;
return 0;
}
开放寻址
「开放寻址 open addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测、多次哈希等。
不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶 None ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在。
为了解决该问题,可以采用「懒删除 lazy deletion」机制:它不直接从哈希表中移除元素,而是利用一个常量 TOMBSTONE
来标记这个桶。在该机制下,None 和 TOMBSTONE
都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE
时应该继续遍历,因为其之下可能还存在键值对。
然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE
的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE
才能找到目标元素。
为此,考虑在线性探测中记录遇到的首个 TOMBSTONE
的索引,并将搜索到的目标元素与该 TOMBSTONE
交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。
#include<bits/stdc++.h>
using namespace std;
/* 键值对 */
struct Pair {
int key;
string val;
Pair(int key, string val) {
this->key = key;
this->val = val;
}
};
/* 开放寻址哈希表 */
class HashMapOpenAddressing {
private:
int size; // 键值对数量
int capacity = 4; // 哈希表容量
const double loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值
const int extendRatio = 2; // 扩容倍数
vector<Pair*> buckets; // 桶数组
Pair* TOMBSTONE = new Pair(-1, "-1"); // 删除标记
public:
/* 构造方法 */
HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {
cout << "调用了 HashMapOpenAddressing 的构造方法" << endl;
}
/* 析构方法 */
~HashMapOpenAddressing() {
cout << "调用了 HashMapOpenAddressing 的析构方法" << endl;
for (Pair* pair : buckets) {
if (pair != nullptr && pair != TOMBSTONE) {
delete pair;
}
}
delete TOMBSTONE;
}
/* 哈希函数 */
int hashFunc(int key) {
return key % capacity;
}
/* 负载因子 */
double loadFactor() {
return (double)size / capacity;
}
/* 搜索 key 对应的桶索引 */
int findBucket(int key) {
int index = hashFunc(key);
//首次标记
int firstTombstone = -1;
// 线性探测,当遇到空桶时跳出
while (buckets[index] != nullptr) {
// 若遇到 key ,返回对应桶索引
if (buckets[index]->key == key) {
// 若之前标记过的,不是第一次删除标记的,则将index对应的键值对移动到firstTombstone索引这里
if (firstTombstone != -1) {
//也就是说现在找到的pair键值对移动到上次删除的键值对桶里。
buckets[firstTombstone] = buckets[index];
//现在的pair键值对就标记为删除了。
buckets[index] = TOMBSTONE;
return firstTombstone; // 因为移动了键值对,就返回移动的那个桶索引
}
return index; // 还没删除过桶索引,默认返回桶索引就行了。
}
//没找到key,且是首个删除标记和当前index索引被标记了TOMBSTONE
if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {
//首次的删除标记被更新了
firstTombstone = index;
}
// 计算桶索引,越过尾部返回头部
index = (index + 1) % capacity;
}
// 若 key 不存在,且还没找到删除标记的桶索引,则返回index索引,否则返回删除标记的索引
return firstTombstone == -1 ? index : firstTombstone;
}
/* 查询操作 */
string get(int key) {
// 搜索 key 对应的桶索引
int index = findBucket(key);
// 若找到键值对,且不时TOMBSTONE标记的,则返回对应 val
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
return buckets[index]->val;
}
// 若键值对不存在,则返回空字符串
return "";
}
/* 添加操作 */
void put(int key, string val) {
// 当负载因子超过阈值时,执行扩容
if (loadFactor() > loadThres) {
extend();
}
// 搜索 key 对应的桶索引
int index = findBucket(key);
// 若找到键值对,且不是TOMBSTONE标记的,则覆盖 val 并返回
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
buckets[index]->val = val;
return;
}
// 若键值对不存在,则添加该键值对
buckets[index] = new Pair(key, val);
size++;
}
/* 删除操作 */
void remove(int key) {
// 搜索 key 对应的桶索引
int index = findBucket(key);
// 若找到键值对,删除它,然后标记TOMBSTONE
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
delete buckets[index];
buckets[index] = TOMBSTONE;
size--;
}
}
int sizeMap() {
return capacity;
}
/* 扩容哈希表 */
void extend() {
// 暂存原哈希表
vector<Pair*> bucketsTmp = buckets;
// 初始化扩容后的新哈希表
capacity *= extendRatio;
buckets = vector<Pair*>(capacity, nullptr);
size = 0;
// 将键值对从原哈希表搬运至新哈希表
for (Pair* pair : bucketsTmp) {
if (pair != nullptr && pair != TOMBSTONE) {
put(pair->key, pair->val);
delete pair;
}
}
}
/* 打印哈希表 */
void print() {
for (Pair* pair : buckets) {
if (pair == nullptr) {
cout << "nullptr" << endl;
}
else if (pair == TOMBSTONE) {
cout << "TOMBSTONE" << endl;
}
else {
cout << pair->key << " -> " << pair->val << endl;
}
}
}
};
int main() {
HashMapOpenAddressing openAddrMap;
openAddrMap.put(0, "0");
openAddrMap.put(1, "1");
openAddrMap.put(2, "2");
openAddrMap.put(3, "3");
openAddrMap.put(4, "4");
string val = openAddrMap.get(2);
cout << "val = " << val << endl;
openAddrMap.remove(3);
openAddrMap.print();
int capacitySize = openAddrMap.sizeMap();
cout << "capacitySize = " << capacitySize << endl;
return 0;
}
哈希算法设计
- 加法哈希:对输入的每个字符的 ASCII 码进行相加,将得到的总和作为哈希值。
- 乘法哈希:利用了乘法的不相关性,每轮乘以一个常数,将各个字符的 ASCII 码累积到哈希值中。
- 异或哈希:将输入数据的每个元素通过异或操作累积到一个哈希值中。
- 旋转哈希:将每个字符的 ASCII 码累积到一个哈希值中,每次累积之前都会对哈希值进行旋转操作。
每种哈希算法的最后一步都是对大质数 1000000007 取模,以确保哈希值在合适的范围内。
当使用大质数作为模数时,可以最大化地保证哈希值的均匀分布。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
总而言之,通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算法的稳健性。
#include<bits/stdc++.h>
using namespace std;
/* 加法哈希 */
int addHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 乘法哈希 */
int mulHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (31 * hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 异或哈希 */
int xorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash ^= (int)c;
}
return hash & MODULUS;
}
/* 旋转哈希 */
int rotHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
}
return (int)hash;
}
int main() {
string str = "360357923174893";
int addNum = addHash(str);
cout << "addNum :" << addNum << endl;
int mulNum = mulHash(str);
cout << "mulNum :" << mulNum << endl;
int xorNum= xorHash(str);
cout << "xorNum :" << xorNum << endl;
int rotNum = rotHash(str);
cout << "rotNum :" << rotNum << endl;
return 0;
}
二叉树
「二叉树 binary tree」是一种非线性数据结构,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。
每个节点都有两个引用(指针),分别指向「左子节点 left-child node」(节点是偶数)和「右子节点 right-child node」(节点是奇数),该节点被称为这两个子节点的「父节点 parent node」。当给定一个二叉树的节点时,将该节点的左子节点及其以下节点形成的树称为该节点的「左子树 left subtree」,同理可得「右子树 right subtree」。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。
- 「根节点 root node」:位于二叉树顶层的节点,没有父节点。
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 None 。
- 「边 edge」:连接两个节点的线段,即节点引用(指针)。
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。
- 节点的「深度 depth」:从根节点到该节点所经过的边的数量。
- 节点的「高度 height」:从最远叶节点到该节点所经过的边的数量。
完美二叉树
完美二叉树又称满二叉树,「完美二叉树 perfect binary tree」所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 0 ,其余所有节点的度都为 2 ;若树高度为 ℎ ,则节点总数为 2^(ℎ+1)−1 ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
若节点的索引为i ,则该节点的左子节点索引为 2i+1 ,右子节点索引为 2i+2,父节点索引为 (i−1)/2(向下取整)。
满二叉树中第n层的节点个数为
2
(
n
−
1
)
2^{(n-1)}
2(n−1)
满二叉树中全部节点的个数为:
2
n
−
1
2^n-1
2n−1
完全二叉树
「完全二叉树 complete binary tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。
完满二叉树
「完满二叉树 full binary tree」除了叶节点之外,其余所有节点都有两个子节点。
平衡二叉树
「平衡二叉树 balanced binary tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
左子树高度 - 右子树高度 = d ,|d| <= 1
搜索二叉树
「二叉搜索树 binary search tree」满足以下条件。
-
- 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值。
-
- 任意节点的左、右子树也是二叉搜索树,即同样满足条件
1.
。
- 任意节点的左、右子树也是二叉搜索树,即同样满足条件
查找节点
给定目标节点值 num
,可以根据二叉搜索树的性质来查找。声明一个节点 cur
,从二叉树的根节点 root
出发,循环比较节点值 cur.val
和 num
之间的大小关系。
- 若
cur.val < num
,说明目标节点在cur
的右子树中,因此执行cur = cur.right
。 - 若
cur.val > num
,说明目标节点在cur
的左子树中,因此执行cur = cur.left
。 - 若
cur.val = num
,说明找到目标节点,跳出循环并返回该节点。
插入节点
- 查找插入位置:与查找操作相似,从根节点出发,根据当前节点值和
num
的大小关系循环向下搜索,直到越过叶节点(遍历至 None )时跳出循环。 - 在该位置插入节点:初始化节点
num
,将该节点置于 None 的位置。
二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。
为了实现插入节点,需要借助节点 pre
保存上一轮循环的节点。这样在遍历至 None 时,可以获取到其父节点,从而完成节点插入操作。
删除节点
先在二叉树中查找到目标节点,再将其从二叉树中删除。
与插入节点类似,需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。
当待删除节点的度为 0 时,表示该节点是叶节点,可以直接删除。
当待删除节点的度为 1 时,将待删除节点替换为其子节点即可。
当待删除节点的度为 2 时,无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左 < 根 < 右”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点。
假设选择右子树的最小节点(即中序遍历的下一个节点),则删除操作流程如下。
- cur找到待删除节点在“中序遍历序列”中的下一个节点,记为
tmp
。 - 将
tmp
的值覆盖待删除节点的值,并在树中递归删除节点tmp
。
二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的
#include<bits/stdc++.h>
using namespace std;
// 搜索二叉树
/* 二叉树节点结构体 */
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
//BFS遍历
void BFS(TreeNode* root) {
//初始化队列
queue<TreeNode*> queue;
queue.push(root);
//保存遍历序列
vector<int>vec;
while (!queue.empty()) {
//队列先进去的节点出队
TreeNode* node = queue.front();
queue.pop();
//存储当前出队节点的值
vec.push_back(node->val);
//当前节点存在左子树,则当前节点的左节点加入队列
if (node->left != nullptr) queue.push(node->left);
//当前节点存在右子树,则当前节点的右节点加入队列
if (node->right != nullptr) queue.push(node->right);
}
cout << "BFS: ";
for (int v : vec) cout << v << " ";
cout << endl;
}
//搜索二叉树,参数num为要寻找的节点值
TreeNode* searchTreeNode(TreeNode* root, int num) {
TreeNode* cur = root;
//左节点的值 < 根节点的值 < 右节点的值
while (cur != nullptr) {
if (cur->val < num) cur = cur->right; //往右节点找
else if (cur->val > num) cur = cur->left; //往左节点找
else break; //找到了,跳出循环
}
return cur;
}
//搜索二叉树 插入节点 参数num为要插入的节点值
void addSearchTreeNode(TreeNode* root, int num) {
//若树为空,则复制上该节点值
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode* cur = root; // 当前节点
TreeNode* pre = nullptr; // 上一个节点
while (cur != nullptr) {
//存在该值,直接返回
if (cur->val == num) return;
//当前的节点不断的赋值给上一个节点,更新上次节点
pre = cur;
//左节点的值 < 根节点的值 < 右节点的值
if (cur->val < num) cur = cur->right;//往右节点找
else cur = cur->left;//往左节点找
}
//找到了pre最后的位置,插入该值
TreeNode* newNode = new TreeNode(num);
if (pre->val < num) pre->right = newNode; //插到pre节点的右边
else pre->left = newNode;//插到pre节点的左边
}
//搜索二叉树 删除节点 参数num为要删除的节点值
void deleSearchTreeNode(TreeNode* root, int num) {
if (root == nullptr) return;
TreeNode* cur = root;
TreeNode* pre = nullptr;
while (cur != nullptr) {
//找到相同的值,跳出循环
if (cur->val == num) break;
pre = cur;//更新上次节点
if (cur->val < num) cur = cur->right;
else cur = cur->left;
}
// 若无待删除节点,则直接返回
if (cur == nullptr) return;
//cur节点的度为0或1时
if (cur->left == nullptr || cur->right == nullptr) {
//child 为cur 的左节点或者cur的右节点
TreeNode* child = cur->left != nullptr ? cur->left : cur->right;
//删除cur节点
if (cur != root) {
if (pre->left == cur) pre->left = child;
else pre->right = child;
}
else root = child; //若删除节点为根节点,则重新指定根节点
delete cur;
}
else {
//cur节点的度为2时
//通过中序遍历拿到cur的下一个节点,右子树最小节点,接就是说遍历cur的左子树
TreeNode* tmpNode = cur->right;
while (tmpNode->left != nullptr) tmpNode = tmpNode->left;
int deleVal = tmpNode->val;
//递归删除tmpNode节点
deleSearchTreeNode(cur, tmpNode->val);
//用tmpNode的值覆盖cur的值
cur->val = deleVal;
}
}
int main() {
/* 初始化二叉树 */
TreeNode* n8 = new TreeNode(8);
TreeNode* n4 = new TreeNode(4);
TreeNode* n12 = new TreeNode(12);
TreeNode* n3 = new TreeNode(3);
TreeNode* n6 = new TreeNode(6);
TreeNode* n10 = new TreeNode(10);
TreeNode* n14 = new TreeNode(14);
TreeNode* n5 = new TreeNode(5);
TreeNode* n7 = new TreeNode(7);
TreeNode* n9 = new TreeNode(9);
TreeNode* n11 = new TreeNode(11);
TreeNode* n13 = new TreeNode(13);
TreeNode* n15 = new TreeNode(15);
// 构建引用指向(即指针)
n8->left = n4;
n8->right = n12;
n4->left = n3;
n4->right = n6;
n12->left = n10;
n12->right = n14;
n6->left = n5;
n6->right = n7;
n10->left = n9;
n10->right = n11;
n14->left = n13;
n14->right = n15;
BFS(n8);
//删除节点4
deleSearchTreeNode(n8, 4);
BFS(n8);
return 0;
}
搜索二叉树的查找元素、插入元素、删除元素的时间复制复杂度均为O(logn)
平衡二叉搜索树AVL
确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 O(logn) 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树 balanced binary search tree」。
“节点高度”是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。
节点的「平衡因子 balance factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。节点平衡因子 = 左子树高度 - 右子树高度
AVL树旋转
旋转操作既能保持“二叉搜索树”的性质,也能使树重新变为“平衡二叉树”,将平衡因子绝对值 >1 的节点称为“失衡节点”,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。
右旋
找到失衡节点,记为node,其左子节点记为child,node节点执行“右旋”操作(在以child节点上为原点顺时针旋转90°),若child右节点存在,记为grandChild,则然后child代替原来node的位置,然后grandChild补在node的左节点上。【node顺转补左】
左旋
找到失衡节点,记为node,其右子节点记为child,node节点执行“左旋”操作(在以child节点上为原点逆时针旋转90°),若child左节点存在,记为grandChild,则然后child代替原来node的位置,然后grandChild补在node的右节点上。【node逆转补右】
可见,右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的
左旋后右旋
仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对 child
执行“左旋”,再对 node
执行“右旋”。
右旋后左旋
对于上述失衡二叉树的镜像情况,需要先对 child
执行“右旋”,然后对 node
执行“左旋”。
四种旋转情况的选择条件:
失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 |
---|---|---|
>1 (即左偏树) | >= 0 | 右旋 |
>1 (即左偏树) | < 0 | 先左旋后右旋 |
<−1 (即右偏树) | <= 0 | 左旋 |
<−1 (即右偏树) | > 0 | 先右旋后左旋 |
#include<bits/stdc++.h>
using namespace std;
// 平衡二叉搜索树 AVL
/* AVL 树节点结构体 */
struct TreeNode {
int val{}; // 节点值
int height = 0; // 节点高度,跟最远叶节点的距离(边)
TreeNode* left{}; // 左子节点
TreeNode* right{}; // 右子节点
TreeNode() = default;
explicit TreeNode(int x) : val(x) {}
};
/* AVL 树 */
class AVLTree {
private:
/* 更新节点高度 */
void updateHeight(TreeNode* node) {
// 节点高度等于最高子树高度 + 1,这个1是包括本身
node->height = max(height(node->left), height(node->right)) + 1;
cout << "updateHight = " << node->height << endl;
}
/* 右旋操作 */
TreeNode* rightRotate(TreeNode* node) {
TreeNode* child = node->left;
TreeNode* grandChild = child->right;
// 以 child 为原点,将 node 向右旋转
child->right = node;
node->left = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
/* 左旋操作 */
TreeNode* leftRotate(TreeNode* node) {
TreeNode* child = node->right;
TreeNode* grandChild = child->left;
// 以 child 为原点,将 node 向左旋转
child->left = node;
node->right = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
/* 执行旋转操作,使该子树重新恢复平衡 */
TreeNode* rotate(TreeNode* node) {
// 获取节点 node 的平衡因子
int _balanceFactor = balanceFactor(node);
// 左偏树
if (_balanceFactor > 1) {
//判断node左节点的平衡因子
if (balanceFactor(node->left) >= 0) {
// 右旋
return rightRotate(node);
}
else {
// 先左旋后右旋
node->left = leftRotate(node->left);
return rightRotate(node);
}
}
// 右偏树
if (_balanceFactor < -1) {
if (balanceFactor(node->right) <= 0) {
// 左旋
return leftRotate(node);
}
else {
// 先右旋后左旋
node->right = rightRotate(node->right);
return leftRotate(node);
}
}
// 平衡树,无须旋转,直接返回
return node;
}
/* 递归插入节点(辅助方法) */
TreeNode* insertHelper(TreeNode* node, int val) {
if (node == nullptr)
return new TreeNode(val);
/* 1. 查找插入位置,并插入节点 */
if (val < node->val)
node->left = insertHelper(node->left, val);
else if (val > node->val)
node->right = insertHelper(node->right, val);
else
return node; // 重复节点不插入,直接返回
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
/* 递归删除节点(辅助方法) */
TreeNode* removeHelper(TreeNode* node, int val) {
if (node == nullptr)
return nullptr;
/* 1. 查找节点,并删除之 */
if (val < node->val)
node->left = removeHelper(node->left, val);
else if (val > node->val)
node->right = removeHelper(node->right, val);
else {
//该节点的度为0或1
if (node->left == nullptr || node->right == nullptr) {
TreeNode* child = node->left != nullptr ? node->left : node->right;
// 子节点数量 = 0 ,直接删除 node 并返回
if (child == nullptr) {
delete node;
return nullptr;
}
// 子节点数量 = 1 ,直接删除 node
else {
delete node;
node = child;
}
}
else {
// 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
TreeNode* temp = node->right;
//node右子树的最小值(左节点上)
while (temp->left != nullptr) {
temp = temp->left;
}
int tempVal = temp->val;
node->right = removeHelper(node->right, temp->val);
node->val = tempVal;
}
}
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
public:
TreeNode* root; // 根节点
/*构造方法*/
AVLTree() : root(nullptr) {
}
/*析构方法*/
~AVLTree() {
freeMemoryTree(root);
}
/* Free the memory allocated to a tree */
void freeMemoryTree(TreeNode* root) {
if (root == nullptr) return;
freeMemoryTree(root->left);
freeMemoryTree(root->right);
// 释放内存
delete root;
}
/* 获取节点高度 */
int height(TreeNode* node) {
// 空节点高度为 -1 ,叶节点高度为 0
return node == nullptr ? -1 : node->height;
}
/* 获取平衡因子 */
int balanceFactor(TreeNode* node) {
// 空节点平衡因子为 0
if (node == nullptr) return 0;
// 节点平衡因子 = 左子树高度 - 右子树高度
return height(node->left) - height(node->right);
}
/* 插入节点 */
void insert(int val) {
root = insertHelper(root, val);
}
/* 删除节点 */
void remove(int val) {
root = removeHelper(root, val);
}
/* 查找节点 */
TreeNode* search(int val) {
TreeNode* cur = root;
// 循环查找,越过叶节点后跳出
while (cur != nullptr) {
// 目标节点在 cur 的右子树中
if (cur->val < val)
cur = cur->right;
// 目标节点在 cur 的左子树中
else if (cur->val > val)
cur = cur->left;
// 找到目标节点,跳出循环
else
break;
}
// 返回目标节点
return cur;
}
//BFS遍历
void BFS(TreeNode* root) {
//初始化队列
queue<TreeNode*> queue;
queue.push(root);
//保存遍历序列
vector<int>vec;
while (!queue.empty()) {
//队列先进去的节点出队
TreeNode* node = queue.front();
queue.pop();
//存储当前出队节点的值
vec.push_back(node->val);
//当前节点存在左子树,则当前节点的左节点加入队列
if (node->left != nullptr) queue.push(node->left);
//当前节点存在右子树,则当前节点的右节点加入队列
if (node->right != nullptr) queue.push(node->right);
}
cout << "BFS: ";
for (int v : vec) cout << v << " ";
cout << endl;
}
};
int main() {
AVLTree avlTree;
avlTree.insert(1);
avlTree.insert(0);
avlTree.insert(4);
avlTree.insert(3);
avlTree.insert(5);
avlTree.insert(6);
avlTree.BFS(avlTree.root);
return 0;
}
二叉树的遍历
二叉树常见的遍历方式包括层序遍历(BFS)、深度优先搜索(DFS)、前序遍历(根左右)、中序遍历(左根右)和后序遍历(左右根)等。前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
广度优先遍历BFS
「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。层序遍历本质上属于「广度优先搜索 breadth-first Search,又称BFS」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
深度优先搜索DFS
深度优先搜索通常基于递归实现,时间复杂O(n)
#include<bits/stdc++.h>
using namespace std;
/* 二叉树节点结构体 */
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
//保存遍历序列
vector<int>vec;
//增加节点6
void addTreeNode(TreeNode* root, TreeNode* p) {
TreeNode* n2 = root->left;
root->left = p;
p->left = n2;
cout << "add node 6" << endl;
}
//删除节点6
void deleTreeNode(TreeNode* root) {
TreeNode* p = root->left;
TreeNode* n2 = p->left;
root->left = n2;
cout << "dele node 6" << endl;
}
//前序遍历 根左右
void preOrder(TreeNode* root) {
if (root == nullptr) return;
vec.push_back(root->val); //保存当前节点的值
preOrder(root->left);// 左节点
preOrder(root->right);//右节点
}
//中序遍历 左根右
void inOrder(TreeNode* root) {
if (root == nullptr) return;
inOrder(root->left);//左节点
vec.push_back(root->val);
inOrder(root->right); //右节点
}
//后序遍历
void postOrder(TreeNode* root) {
if (root == nullptr) return;
postOrder(root->left);
postOrder(root->right);
vec.push_back(root->val);
}
//BFS遍历
void BFS(TreeNode* root) {
//初始化队列
queue<TreeNode*> queue;
queue.push(root);
//保存遍历序列
vector<int>vec;
while (!queue.empty()) {
//队列先进去的节点出队
TreeNode* node = queue.front();
queue.pop();
//存储当前出队节点的值
vec.push_back(node->val);
//当前节点存在左子树,则当前节点的左节点加入队列
if (node->left != nullptr) queue.push(node->left);
//当前节点存在右子树,则当前节点的右节点加入队列
if (node->right != nullptr) queue.push(node->right);
}
cout << "BFS: ";
for (int v : vec) cout << v << " ";
cout << endl;
}
int main() {
/* 初始化二叉树 */
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
BFS(n1);
preOrder(n1);
cout << "preOrder: ";
for (int v : vec) cout << v << " ";
cout << endl;
vec.clear();
inOrder(n1);
cout << "inOrder: ";
for (int v : vec) cout << v << " ";
cout << endl;
vec.clear();
postOrder(n1);
cout << "postOrder: ";
for (int v : vec) cout << v << " ";
cout << endl;
TreeNode* addTree = new TreeNode(6);
addTreeNode(n1, addTree);
BFS(n1);
deleTreeNode(n1);
BFS(n1);
return 0;
}
Heap堆
「堆 heap」是一种满足特定条件的完全二叉树。
- 「大顶堆 max heap」:任意节点的值 ≥ 其子节点的值。
- 「小顶堆 min heap」:任意节点的值 ≤ 其子节点的值。
- 将二叉树的根节点称为“堆顶”,将底层最靠右的节点称为“堆底”。
- 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的。
堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列。
#include<bits/stdc++.h>
using namespace std;
int main() {
// 初始化小顶堆,升序队列,从小到大
priority_queue<int, vector<int>, greater<int>> minHeap;
// 初始化大顶堆,降序队列,从大到小
priority_queue<int, vector<int>, less<int>> maxHeap;
/* 元素入堆 */
maxHeap.push(1);
maxHeap.push(3);
maxHeap.push(2);
maxHeap.push(5);
maxHeap.push(4);
/* 获取堆顶元素 */
int peek = maxHeap.top(); // 5
/* 堆顶元素出堆 */
// 出堆元素会形成一个从大到小的序列
maxHeap.pop(); // 5
maxHeap.pop(); // 4
maxHeap.pop(); // 3
maxHeap.pop(); // 2
maxHeap.pop(); // 1
/* 获取堆大小 */
int size = maxHeap.size();
/* 判断堆是否为空 */
bool isEmpty = maxHeap.empty();
/* 输入列表并建堆 */
vector<int> input{ 1, 3, 2, 5, 4 };
priority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());
return 0;
}
堆底元素入堆
给定元素 val
,首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为「堆化 heapify」。
考虑从入堆节点开始,从底至顶执行堆化。比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。【一步步往上换】
设节点总数为 n ,则树的高度为 O(logn) 。由此可知,堆化操作的循环轮数最多为 O(logn) ,元素入堆操作的时间复杂度为 O(logn) 。
#include<bits/stdc++.h>
using namespace std;
//将采用数组来存储堆。实现大堆顶
vector<int>maxHeap;
//若节点的索引为i ,则该节点的左子节点索引为 2i+1 ,右子节点索引为 2i+2,父节点索引为 (i−1)/2(向下取整)。
/* 获取左子节点索引 */
int left(int i) {
return 2 * i + 1;
}
/* 获取右子节点索引 */
int right(int i) {
return 2 * i + 2;
}
/* 获取父节点索引 */
int parent(int i) {
return (i - 1) / 2; // 向下取整
}
/* 访问堆顶元素 */
int peek() {
return maxHeap[0];
}
/* 获取堆大小 */
int heapSize() {
return maxHeap.size();
}
/* 从节点 i 开始,从底至顶堆化 */
void siftUp(int i) {
while (true) {
// 获取节点 i 的父节点
int p = parent(i);
// 当“越过根节点”或“节点无须修复”时,结束堆化
if (p < 0 || maxHeap[i] <= maxHeap[p]) break;
// 交换两节点
swap(maxHeap[i], maxHeap[p]);
// 循环向上堆化
i = p;
}
}
/* 元素入堆 */
void push(int val) {
// 添加节点
maxHeap.push_back(val);
// 从底至顶堆化
siftUp(heapSize() - 1);
}
int main() {
push(1);
push(3);
push(2);
push(7);
push(5);
push(9);
push(6);
push(10);
for (int v : maxHeap) cout << v << " ";
cout << endl;
int peeknNum = peek();
cout << "peeknNum = " << peeknNum << endl;
return 0;
}
堆顶元素出堆
- 交换堆顶元素与堆底元素(即交换根节点与最右叶节点)。
- 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
- 从根节点开始,从顶至底执行堆化。
“从顶至底堆化”的操作方向与“从底至顶堆化”相反,将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。堆顶元素出堆操作的时间复杂度也为 O(logn) 。
#include<bits/stdc++.h>
using namespace std;
//将采用数组来存储堆。实现大堆顶
vector<int>maxHeap;
//若节点的索引为i ,则该节点的左子节点索引为 2i+1 ,右子节点索引为 2i+2,父节点索引为 (i−1)/2(向下取整)。
/* 获取左子节点索引 */
int left(int i) {
return 2 * i + 1;
}
/* 获取右子节点索引 */
int right(int i) {
return 2 * i + 2;
}
/* 获取父节点索引 */
int parent(int i) {
return (i - 1) / 2; // 向下取整
}
/* 访问堆顶元素 */
int peek() {
return maxHeap[0];
}
/* 获取堆大小 */
int heapSize() {
return maxHeap.size();
}
/* 从节点 i 开始,从底至顶堆化 */
void siftUp(int i) {
while (true) {
// 获取节点 i 的父节点
int p = parent(i);
// 当“越过根节点”或“节点无须修复”时,结束堆化
if (p < 0 || maxHeap[i] <= maxHeap[p]) break;
// 交换两节点
swap(maxHeap[i], maxHeap[p]);
// 循环向上堆化
i = p;
}
}
/* 元素入堆 */
void push(int val) {
// 添加节点
maxHeap.push_back(val);
// 从底至顶堆化
siftUp(heapSize() - 1);
}
/* 从节点 i 开始,从顶至底堆化 */
void siftDown(int i) {
while (true) {
// 判断节点 i, l, r 中值最大的节点,记为 ma
int l = left(i), r = right(i), ma = i;
if (l < heapSize() && maxHeap[l] > maxHeap[ma])
ma = l;
if (r < heapSize() && maxHeap[r] > maxHeap[ma])
ma = r;
// 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if (ma == i)
break;
//最大值和当前的值交换
swap(maxHeap[i], maxHeap[ma]);
// 循环向下堆化
i = ma;
}
}
bool isEmpty() {
return heapSize() == 0;
}
/* 元素出堆 */
void pop() {
// 判空处理
if (isEmpty()) {
throw out_of_range("heap is null");
}
// 交换根节点与最右叶节点(即交换首元素与尾元素)
swap(maxHeap[0], maxHeap[heapSize() - 1]);
// 删除节点
maxHeap.pop_back();
// 从顶至底堆化
siftDown(0);
}
int main() {
push(1);
push(3);
push(2);
push(7);
push(5);
push(9);
push(6);
push(10);
for (int v : maxHeap) cout << v << " ";
cout << endl;
int peeknNum = peek();
cout << "peeknNum = " << peeknNum << endl;
pop();
pop();
for (int v : maxHeap) cout << v << " ";
cout << endl;
int peeknNum2 = peek();
cout << "peeknNum2 = " << peeknNum2 << endl;
return 0;
}
建堆操作
使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。
首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化。
每当一个元素入堆,堆的长度就加一。由于节点是从顶到底依次被添加进二叉树的,因此堆是“自上而下”地构建的。
设元素数量为n ,每个元素的入堆操作使用 O(logn) 时间,因此该建堆方法的时间复杂度为 O(nlogn) 。但这个估算结果斌不准确,因没考虑到二叉树底层节点数量远多于顶层节点的性质。
实现一种更为高效的建堆方法,共分为两步。
-
- 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。
-
- 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。
每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆。而由于是倒序遍历,因此堆是“自下而上”地被构建的。
叶节点没有子节点,天然就是合法的子堆,因此无需堆化。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,从它开始倒序遍历并执行堆化。
节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,可以将各层的“节点数量 × 节点高度”求和,从而得到所有节点的堆化迭代次数的总和。
T
(
h
)
=
2
0
h
+
2
1
(
h
−
1
)
+
2
2
(
h
−
2
)
+
.
.
.
+
2
(
h
−
1
)
×
1
T(h)=2^0h+2^1(h-1)+2^2(h-2)+...+2^{(h-1)}\times1
T(h)=20h+21(h−1)+22(h−2)+...+2(h−1)×1
得到:
T
(
h
)
=
2
h
+
1
−
h
−
2
=
O
(
2
h
)
=
O
(
n
)
T(h)=2^{h+1}-h-2=O(2^h)=O(n)
T(h)=2h+1−h−2=O(2h)=O(n)
所以输入列表并建堆的时间复杂度为 O(n)
#include<bits/stdc++.h>
using namespace std;
//将采用数组来存储堆。实现大堆顶
vector<int>maxHeap;
//若节点的索引为i ,则该节点的左子节点索引为 2i+1 ,右子节点索引为 2i+2,父节点索引为 (i−1)/2(向下取整)。
/* 获取左子节点索引 */
int left(int i) {
return 2 * i + 1;
}
/* 获取右子节点索引 */
int right(int i) {
return 2 * i + 2;
}
/* 获取父节点索引 */
int parent(int i) {
return (i - 1) / 2; // 向下取整
}
/* 访问堆顶元素 */
int peek() {
return maxHeap[0];
}
/* 获取堆大小 */
int heapSize() {
return maxHeap.size();
}
/* 从节点 i 开始,从顶至底堆化 */
void siftDown(int i) {
while (true) {
// 判断节点 i, l, r 中值最大的节点,记为 ma
int l = left(i), r = right(i), ma = i;
if (l < heapSize() && maxHeap[l] > maxHeap[ma])
ma = l;
if (r < heapSize() && maxHeap[r] > maxHeap[ma])
ma = r;
// 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if (ma == i)
break;
//最大值和当前的值交换
swap(maxHeap[i], maxHeap[ma]);
// 循环向下堆化
i = ma;
}
}
/*根据输入列表建堆 */
void buildHeap(vector<int> nums) {
// 将列表元素原封不动添加进堆
maxHeap = nums;
// 堆化除叶节点以外的其他所有节点
for (int i = parent(heapSize() - 1); i >= 0; i--) {
siftDown(i);
}
}
int main() {
vector<int>nums = { 1,3,2,7,5,9,6,10 };
buildHeap(nums);
for (int v : maxHeap) cout << v << " ";
cout << endl;
int peeknNum = peek();
cout << "peeknNum = " << peeknNum << endl;
return 0;
}
Top-k问题
Q : 给定一个长度为 n 无序数组 nums ,请返回数组中前 k 大的元素。
#include<bits/stdc++.h>
using namespace std;
/* 基于堆查找数组中最大的 k 个元素,建立小堆顶 */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> heap;
// 将数组的前 k 个元素入堆
for (int i = 0; i < k; i++) {
heap.push(nums[i]);
}
// 从第 k+1 个元素开始,保持堆的长度为 k
for (int i = k; i < nums.size(); i++) {
// 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
if (nums[i] > heap.top()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
int main() {
//给定一个长度为 n 无序数组 nums ,请返回数组中前 k 大的元素
int k = 3;
vector<int>nums = { 1,2,3,5,6,7,8,9 };
priority_queue<int, vector<int>, greater<int>> heap = topKHeap(nums, k);;
while (!heap.empty()) {
cout << heap.top() << ' ';
heap.pop();
}
cout << endl;
return 0;
}
图
「图 graph」是一种非线性数据结构,由**「顶点 vertex」**和「边 edge」组成。可以将图 G 抽象地表示为一组顶点 V 和一组边 E 的集合。即G={V,E}
根据边是否具有方向,可分为**「无向图 undirected graph」和「有向图 directed graph」**。
根据所有顶点是否连通,可分为**「连通图 connected graph」和「非连通图 disconnected graph」**。
- 对于连通图,从某个顶点出发,可以到达其余任意顶点。
- 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。
还可以为边添加“权重”变量,从而得到有权图 weighted graph。
- 「邻接 adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。
- 「路径 path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。
- 「度 degree」:一个顶点拥有的边数。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。【入度指向自己,出度指向别人】
图的常用表示方式包括邻接矩阵和邻接表。
邻接矩阵
设图的顶点数量为 n ,「邻接矩阵 adjacency matrix」使用一个 n×n
大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 1 或 0 表示两个顶点之间是否存在边。
设邻接矩阵为M、顶点列表为 V ,那么矩阵元素M[i,j]=1
表示顶点V[i]
到顶点 V[j]
之间存在边,反之M[i,j]=0
表示两顶点之间无边。
邻接矩阵具有以下特性。
- 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义。
- 对于无向图,两个方向的边等价,此时邻接矩阵关于主对角线对称。
- 将邻接矩阵的元素从 1 和 0 替换为权重,则可表示有权图。
使用邻接矩阵表示图时,可以直接访问矩阵元素以获取边,因此增删查操作的效率很高,时间复杂度均为 O(1) 。矩阵的空间复杂度为 O(n^2) ,内存占用较多。
邻接表
「邻接表 adjacency list」使用 n 个链表来表示图,链表节点表示顶点。第 i 条链表对应顶点 i ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。
图的基本操作
图的基础操作可分为对“边”的操作和对“顶点”的操作。
基于邻接矩阵的实现
给定一个顶点数量为 n 的无向图。
- 添加或删除边:直接在邻接矩阵中修改指定的边即可,使用 O(1) 时间。而由于是无向图,因此需要同时更新两个方向的边。
- 添加顶点:在邻接矩阵的尾部添加一行一列,并全部填 0 即可,使用 O(n) 时间。
- 删除顶点:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 (n−1)^2 个元素“向左上移动”,从而使用 O(n^2) 时间。
- 初始化:传入 n 个顶点,初始化长度为 n 的顶点列表
vertices
,使用 O(n) 时间;初始化 n×n 大小的邻接矩阵adjMat
,使用 O(n^2) 时间。
#include<bits/stdc++.h>
using namespace std;
/* 图:邻接矩阵 */
class GraphAdjMat {
vector<int> vertices;//顶点列表,元素代表顶点值,索引代表顶点索引
vector<vector<int>>adjMat;//邻接矩阵,为二维数组
public:
//构造方法 edges 元素代表顶点索引,即对应 vertices 元素索引
GraphAdjMat(const vector<int>& vertices, const vector<vector<int>>& edges) {
//添加顶点
for (int val : vertices) {
addVertex(val);
}
//添加边
for (const vector<int>& edge : edges) {
addEdge(edge[0], edge[1]);
}
}
//获取顶点列表的顶点数量
int verticesSize() const {
return vertices.size();
}
//添加顶点 邻接矩阵中后面添加一行一列
void addVertex(int val) {
//获取顶点列表的顶点数量
int n = verticesSize();
vertices.push_back(val);
//在邻接矩阵中添加一行
adjMat.emplace_back(vector<int>(n, 0));
//在邻接矩阵中添加一列
for (vector<int>& row : adjMat) {
row.push_back(0);
}
}
//删除顶点 邻接矩阵中删除一行一列,该点的右下角往左上角移动
void removeVertex(int index) {
//该顶点不存在
if (index > verticesSize()) throw out_of_range("Out of range");
//在顶点列表中删除这个索引对应的元素
vertices.erase(vertices.begin() + index);
//在邻接矩阵中删除这个索引的一行
adjMat.erase(adjMat.begin() + index);
//在邻接矩阵中删除索引这一列
for (vector<int>& row : adjMat) {
row.erase(row.begin() + index);
}
}
//添加边 (两个顶点之间连线,在二维数组中确定一个坐标点即可)参数i,j对应顶点列表vertices元素的索引
void addEdge(int i, int j) {
//索引越界与相等处理,同个顶点没有边
if (i < 0 || j < 0 || i >= verticesSize() || j >= verticesSize() || i == j) {
throw out_of_range("Out of range");
}
//两顶点之间能连线,添加边赋值为1,且对称
adjMat[i][j] = 1;
adjMat[j][i] = 1;
}
//删除边 (两个顶点之间连线,在二维数组中确定一个坐标点即可)参数i,j对应顶点列表vertices元素的索引
void removeEdge(int i, int j) {
//索引越界与相等处理,同个顶点没有边
if (i < 0 || j < 0 || i >= verticesSize() || j >= verticesSize() || i == j) {
throw out_of_range("Out of range");
}
//两顶点之间能连线,删除边赋值为0,且对称
adjMat[i][j] = 0;
adjMat[j][i] = 0;
}
/******************************** 打印信息辅助函数 开始 *************************************************/
/* Concatenate a vector with a delim */
template <typename T> string strJoin(const string& delim, const T& vec) {
ostringstream s;
for (const auto& i : vec) {
if (&i != &vec[0]) {
s << delim;
}
s << i;
}
return s.str();
}
/* Get the Vector String object */
template <typename T> string getVectorString(vector<T>& list) {
return "[" + strJoin(", ", list) + "]";
}
/* Print a vector */
template <typename T> void printVector(vector<T> list) {
cout << getVectorString(list) << '\n';
}
/* Print a vector matrix */
template <typename T> void printVectorMatrix(vector<vector<T>>& matrix) {
cout << "[" << '\n';
for (vector<T>& list : matrix)
cout << " " + getVectorString(list) + "," << '\n';
cout << "]" << '\n';
}
/* 打印邻接矩阵 */
void print() {
cout << "顶点列表 = ";
printVector(vertices);
cout << "邻接矩阵 =" << endl;
printVectorMatrix(adjMat);
}
/******************************** 打印信息辅助函数 结束 *************************************************/
};
int main() {
/* 初始化无向图 */
// 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引
vector<int> vertices = { 1, 3, 2, 5, 4 };
vector<vector<int>> edges = { {0, 1}, {0, 3}, {1, 2}, {2, 3}, {2, 4}, {3, 4} }; //两个顶点之间的连线
GraphAdjMat graph(vertices, edges);
cout << "\n初始化后,图为" << endl;
graph.print();
/* 添加边 */
// 顶点 1, 2 的索引分别为 0, 2
graph.addEdge(0, 2);
cout << "\n添加边 1-2 后,图为" << endl;
graph.print();
/* 删除边 */
// 顶点 1, 3 的索引分别为 0, 1
graph.removeEdge(0, 1);
cout << "\n删除边 1-3 后,图为" << endl;
graph.print();
/* 添加顶点 */
graph.addVertex(6);
cout << "\n添加顶点 6 后,图为" << endl;
graph.print();
/* 删除顶点 */
// 顶点 3 的索引为 1
graph.removeVertex(1);
cout << "\n删除顶点 3 后,图为" << endl;
graph.print();
return 0;
}
基于邻接表的实现
设无向图的顶点总数为 n、边总数为 m
- 添加边:在顶点对应链表的末尾添加边即可,使用 O(1) 时间。因为是无向图,所以需要同时添加两个方向的边。
- 删除边:在顶点对应链表中查找并删除指定边,使用 O(m) 时间。在无向图中,需要同时删除两个方向的边。
- 添加顶点:在邻接表中添加一个链表,并将新增顶点作为链表头节点,使用 O(1) 时间。
- 删除顶点:需遍历整个邻接表,删除包含指定顶点的所有边,使用 O(n+m) 时间。
- 初始化:在邻接表中创建 n 个顶点和 2m 条边,使用 O(n+m) 时间。
#include<bits/stdc++.h>
using namespace std;
/* 图:邻接表 */
/* 顶点类 */
struct Vertex {
int val;
Vertex(int x) : val(x) {
}
};
/* 输入值列表 vals ,返回顶点列表 vets */
vector<Vertex*> valsToVets(vector<int> vals) {
vector<Vertex*> vets;
for (int val : vals) {
vets.push_back(new Vertex(val));
}
return vets;
}
/* 输入顶点列表 vets ,返回值列表 vals */
vector<int> vetsToVals(vector<Vertex*> vets) {
vector<int> vals;
for (Vertex* vet : vets) {
vals.push_back(vet->val);
}
return vals;
}
class GraphAdjList {
public:
//邻接表 key为顶点,value为该顶点的所有邻接顶点
unordered_map<Vertex*, vector<Vertex*>>adjList;
/* 构造方法 */
GraphAdjList(const vector<vector<Vertex*>>& edges) {
// 添加所有顶点和边
for (const vector<Vertex*>& edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 获取顶点Vertex数量 */
int vertexSize() {
return adjList.size();
}
/* 添加边 vet1和vet2都是顶点值*/
void addEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 添加边 vet1 - vet2 (作为key,value添加另一方的值)
adjList[vet1].push_back(vet2);
adjList[vet2].push_back(vet1);
}
/* 删除边 vet1和vet2都是顶点值 */
void removeEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 删除边 vet1 - vet2 (作为key,value删除另一方的值)
remove(adjList[vet1], vet2);
remove(adjList[vet2], vet1);
}
//删除指定节点 在adjList(vec)指定的key (顶点值vet)中删除指定节点
void remove(vector<Vertex*>& vec, Vertex* vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
vec.erase(vec.begin() + i);
break;
}
}
}
/* 添加顶点 在邻接表adjList中查找是否存在这个key里顶点vet */
void addVertex(Vertex* vet) {
//若存在这个key(顶点vet)返回1,不存在返回0
if (adjList.count(vet)) return;
//否则不存在就在邻接表中添加一个新链表
adjList[vet] = vector<Vertex*>();
}
/* 删除顶点 */
void removeVertex(Vertex* vet) {
//不存在这个key(顶点vet)
if (!adjList.count(vet))
throw invalid_argument("The Vertex not found.");
//否则存在,则在邻接表中删除顶点 vet 对应的链表
adjList.erase(vet);
//遍历其他顶点的链表,删除所有包含 vet 的边
for (auto& adj : adjList) {
remove(adj.second, vet);
}
}
/******************************** 打印信息辅助函数 开始 *************************************************/
/* Concatenate a vector with a delim */
template <typename T> string strJoin(const string& delim, const T& vec) {
ostringstream s;
for (const auto& i : vec) {
if (&i != &vec[0]) {
s << delim;
}
s << i;
}
return s.str();
}
/* Get the Vector String object */
template <typename T> string getVectorString(vector<T>& list) {
return "[" + strJoin(", ", list) + "]";
}
/* Print a vector */
template <typename T> void printVector(vector<T> list) {
cout << getVectorString(list) << '\n';
}
/* 打印邻接表 */
void print() {
cout << "邻接表 =" << endl;
for (auto& adj : adjList) {
const auto& key = adj.first;
const auto& vec = adj.second;
cout << key->val << ": ";
printVector(vetsToVals(vec));
}
}
/******************************** 打印信息辅助函数 结束 *************************************************/
};
int main() {
/* 初始化无向图 */
vector<Vertex*> v = valsToVets(vector<int>{1, 3, 2, 5, 4});
vector<vector<Vertex*>> edges = { {v[0], v[1]}, {v[0], v[3]}, {v[1], v[2]},
{v[2], v[3]}, {v[2], v[4]}, {v[3], v[4]} };
GraphAdjList graph(edges);
cout << "\n初始化后,图为" << endl;
graph.print();
/******************** 执行边相关的函数时,注释掉执行顶点的相关操作*****************************/
///* 添加边 */
顶点 1, 2 即 v[0], v[2]
//graph.addEdge(v[0], v[2]);
//cout << "\n添加边 1-2 后,图为" << endl;
//graph.print();
///* 删除边 */
顶点 1, 3 即 v[0], v[1]
//graph.removeEdge(v[0], v[1]);
//cout << "\n删除边 1-3 后,图为" << endl;
//graph.print();
/******************** 执行边相关的函数时,注释掉执行顶点的相关操作 *****************************/
/* 添加顶点 */
Vertex* v5 = new Vertex(6);
graph.addVertex(v5);
cout << "\n添加顶点 6 后,图为" << endl;
graph.print();
/* 删除顶点 */
// 顶点 3 即 v[1]
graph.removeVertex(v[1]);
cout << "\n删除顶点 3 后,图为" << endl;
graph.print();
// 释放内存
for (Vertex* vet : v) {
delete vet;
}
return 0;
}
设图中共有 n 个顶点和 m 条边。
邻接矩阵 | 邻接表(链表) | 邻接表(哈希表) | |
---|---|---|---|
判断是否邻接 | O(1) | O(m) | O(1) |
添加边 | O(1) | O(1) | O(1) |
删除边 | O(1) | O(m) | O(1) |
添加顶点 | O(n) | O(1) | O(1) |
删除顶点 | O(n^2) | O(n+m) | O(n) |
内存空间占用 | O(n^2) | O(n+m) | O(n+m) |
综合来看,邻接矩阵体现了“以空间换时间”的原则,而邻接表体现了“以时间换空间”的原则。
图的遍历
图的遍历方式可分为两种:「广度优先遍历 breadth-first traversal」和「深度优先遍历 depth-first traversal」。它们也常被称为「广度优先搜索 breadth-first search」和「深度优先搜索 depth-first search」,简称 BFS 和 DFS 。
广度优先搜索BFS
广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张。从左上角顶点出发,先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。
0-----1-----2
| | |
3-----4-----5
| | |
6-----7-----8
BFS 遍历后得到序列为: 0,1,3,2,4,6,5,7,8
BFS 通常借助队列来实现。队列具有“先入先出”的性质,这与 BFS 的“由近及远”的思想异曲同工。故广度优先遍历的序列不是唯一的
- 将遍历起始顶点
startVet
加入队列,并开启循环。 - 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。
- 循环步骤
2.
,直到所有顶点被访问完成后结束。
为了防止重复遍历顶点,需要借助一个哈希表 visited
来记录哪些节点已被访问。
#include<bits/stdc++.h>
using namespace std;
/* 图:广度优先遍历BFS */
/* 顶点类 */
struct Vertex {
int val;
Vertex(int x) : val(x) {
}
};
/* 输入值列表 vals ,返回顶点列表 vets */
vector<Vertex*> valsToVets(vector<int> vals) {
vector<Vertex*> vets;
for (int val : vals) {
vets.push_back(new Vertex(val));
}
return vets;
}
/* 输入顶点列表 vets ,返回值列表 vals */
vector<int> vetsToVals(vector<Vertex*> vets) {
vector<int> vals;
for (Vertex* vet : vets) {
vals.push_back(vet->val);
}
return vals;
}
class GraphAdjList {
public:
//邻接表 key为顶点,value为该顶点的所有邻接顶点
unordered_map<Vertex*, vector<Vertex*>>adjList;
/* 构造方法 */
GraphAdjList(const vector<vector<Vertex*>>& edges) {
// 添加所有顶点和边
for (const vector<Vertex*>& edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 获取顶点Vertex数量 */
int vertexSize() {
return adjList.size();
}
/* 添加边 vet1和vet2都是顶点值*/
void addEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 添加边 vet1 - vet2 (作为key,value添加另一方的值)
adjList[vet1].push_back(vet2);
adjList[vet2].push_back(vet1);
}
/* 删除边 vet1和vet2都是顶点值 */
void removeEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 删除边 vet1 - vet2 (作为key,value删除另一方的值)
remove(adjList[vet1], vet2);
remove(adjList[vet2], vet1);
}
//删除指定节点 在adjList(vec)指定的key (顶点值vet)中删除指定节点
void remove(vector<Vertex*>& vec, Vertex* vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
vec.erase(vec.begin() + i);
break;
}
}
}
/* 添加顶点 在邻接表adjList中查找是否存在这个key里顶点vet */
void addVertex(Vertex* vet) {
//若存在这个key(顶点vet)返回1,不存在返回0
if (adjList.count(vet)) return;
//否则不存在就在邻接表中添加一个新链表
adjList[vet] = vector<Vertex*>();
}
/* 删除顶点 */
void removeVertex(Vertex* vet) {
//不存在这个key(顶点vet)
if (!adjList.count(vet))
throw invalid_argument("The Vertex not found.");
//否则存在,则在邻接表中删除顶点 vet 对应的链表
adjList.erase(vet);
//遍历其他顶点的链表,删除所有包含 vet 的边
for (auto& adj : adjList) {
remove(adj.second, vet);
}
}
/* 广度优先遍历 BFS 参数一是整个图,参数二是开始递起点的顶点*/
vector<Vertex*> graphBFS(GraphAdjList& graph, Vertex* startVet) {
//顶点遍历的序列
vector<Vertex*>res;
//哈希表,用于记录已被访问过的顶点,值不重复
unordered_set<Vertex*>visited = { startVet };
//实现一个队列,先进先出
queue<Vertex*>que;
que.push(startVet);
//从顶点开始,然后不断的从队列里取,直至完成
while (!que.empty()) {
//取出队列的首个顶点
Vertex* vet = que.front();
que.pop();//删除队列的首个顶点
res.push_back(vet);//记录已经访问过的顶点
//遍历该顶点的所有邻接顶点
cout << "顶点 vet = " << vet->val << " add: \n";
for (auto adjVet : graph.adjList[vet]) {
//查询这个顶点是否已经被访问过,存在返回1,不存在返回0
if (visited.count(adjVet)) continue;// 跳过已被访问过的顶点
else {
cout << " current add Vet = " << adjVet->val << "\n";
visited.emplace(adjVet); //标记该顶点已被访问
que.push(adjVet); // 未访问的顶点添加到队列里
}
}
for (unordered_set<Vertex*>::iterator it = visited.begin(); it != visited.end(); it++)
cout << *&(*it)->val << " ";
cout << endl;
}
return res;
}
/******************************** 打印信息辅助函数 开始 *************************************************/
/* Concatenate a vector with a delim */
template <typename T> string strJoin(const string& delim, const T& vec) {
ostringstream s;
for (const auto& i : vec) {
if (&i != &vec[0]) {
s << delim;
}
s << i;
}
return s.str();
}
/* Get the Vector String object */
template <typename T> string getVectorString(vector<T>& list) {
return "[" + strJoin(", ", list) + "]";
}
/* Print a vector */
template <typename T> void printVector(vector<T> list) {
cout << getVectorString(list) << '\n';
}
/* 打印邻接表 */
void print() {
cout << "邻接表 =" << endl;
for (auto& adj : adjList) {
const auto& key = adj.first;
const auto& vec = adj.second;
cout << key->val << ": ";
printVector(vetsToVals(vec));
}
}
/******************************** 打印信息辅助函数 结束 *************************************************/
};
int main() {
/* 初始化无向图 */
vector<Vertex*> v = valsToVets({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
vector<vector<Vertex*>> edges = { {v[0], v[1]}, {v[0], v[3]}, {v[1], v[2]}, {v[1], v[4]},
{v[2], v[5]}, {v[3], v[4]}, {v[3], v[6]}, {v[4], v[5]},
{v[4], v[7]}, {v[5], v[8]}, {v[6], v[7]}, {v[7], v[8]} };
GraphAdjList graph(edges);
cout << "\n初始化后,图为\n";
graph.print();
/* 广度优先遍历 BFS */
vector<Vertex*> res = graph.graphBFS(graph, v[0]);
cout << "\n广度优先遍历(BFS)顶点序列为" << endl;
graph.printVector(vetsToVals(res));
// 释放内存
for (Vertex* vet : v) {
delete vet;
}
return 0;
}
时间复杂度: 所有顶点都会入队并出队一次,使用 O(|V|) 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问 2 次,使用 O(2|E|) 时间;总体使用 O(|V|+|E|) 时间。
空间复杂度: 列表 res
,哈希表 visited
,队列 que
中的顶点数量最多为 |V| ,使用 O(|V|) 空间。
深度优先搜索DFS
深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。从左上角顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,而在返回的途中会查询本次当前所在节点是否存在别的邻接节点,若有则再继续走到尽头并返回,以此类推,直至所有顶点遍历完成,不一定是原路返回。
这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中也需要借助一个哈希表 visited
来记录已被访问的顶点,以避免重复访问顶点。深度优先遍历序列的顺序也不是唯一的
#include<bits/stdc++.h>
using namespace std;
/* 图:深度优先遍历DFS */
/* 顶点类 */
struct Vertex {
int val;
Vertex(int x) : val(x) {
}
};
/* 输入值列表 vals ,返回顶点列表 vets */
vector<Vertex*> valsToVets(vector<int> vals) {
vector<Vertex*> vets;
for (int val : vals) {
vets.push_back(new Vertex(val));
}
return vets;
}
/* 输入顶点列表 vets ,返回值列表 vals */
vector<int> vetsToVals(vector<Vertex*> vets) {
vector<int> vals;
for (Vertex* vet : vets) {
vals.push_back(vet->val);
}
return vals;
}
class GraphAdjList {
public:
//邻接表 key为顶点,value为该顶点的所有邻接顶点
unordered_map<Vertex*, vector<Vertex*>>adjList;
/* 构造方法 */
GraphAdjList(const vector<vector<Vertex*>>& edges) {
// 添加所有顶点和边
for (const vector<Vertex*>& edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 获取顶点Vertex数量 */
int vertexSize() {
return adjList.size();
}
/* 添加边 vet1和vet2都是顶点值*/
void addEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 添加边 vet1 - vet2 (作为key,value添加另一方的值)
adjList[vet1].push_back(vet2);
adjList[vet2].push_back(vet1);
}
/* 删除边 vet1和vet2都是顶点值 */
void removeEdge(Vertex* vet1, Vertex* vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("The Vertex not found.");
// 删除边 vet1 - vet2 (作为key,value删除另一方的值)
remove(adjList[vet1], vet2);
remove(adjList[vet2], vet1);
}
//删除指定节点 在adjList(vec)指定的key (顶点值vet)中删除指定节点
void remove(vector<Vertex*>& vec, Vertex* vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
vec.erase(vec.begin() + i);
break;
}
}
}
/* 添加顶点 在邻接表adjList中查找是否存在这个key里顶点vet */
void addVertex(Vertex* vet) {
//若存在这个key(顶点vet)返回1,不存在返回0
if (adjList.count(vet)) return;
//否则不存在就在邻接表中添加一个新链表
adjList[vet] = vector<Vertex*>();
}
/* 删除顶点 */
void removeVertex(Vertex* vet) {
//不存在这个key(顶点vet)
if (!adjList.count(vet))
throw invalid_argument("The Vertex not found.");
//否则存在,则在邻接表中删除顶点 vet 对应的链表
adjList.erase(vet);
//遍历其他顶点的链表,删除所有包含 vet 的边
for (auto& adj : adjList) {
remove(adj.second, vet);
}
}
/* 广度优先遍历 BFS 参数一是整个图,参数二是开始递起点的顶点*/
vector<Vertex*> graphBFS(GraphAdjList& graph, Vertex* startVet) {
//顶点遍历的序列
vector<Vertex*>res;
//哈希表,用于记录已被访问过的顶点,值不重复
unordered_set<Vertex*>visited = { startVet };
//实现一个队列,先进先出
queue<Vertex*>que;
que.push(startVet);
//从顶点开始,然后不断的从队列里取,直至完成
while (!que.empty()) {
//取出队列的首个顶点
Vertex* vet = que.front();
que.pop();//删除队列的首个顶点
res.push_back(vet);//记录已经访问过的顶点
//遍历该顶点的所有邻接顶点
cout << "顶点 vet = " << vet->val << " add: \n";
for (auto adjVet : graph.adjList[vet]) {
//查询这个顶点是否已经被访问过,存在返回1,不存在返回0
if (visited.count(adjVet)) continue;// 跳过已被访问过的顶点
else {
cout << " current add Vet = " << adjVet->val << "\n";
visited.emplace(adjVet); //标记该顶点已被访问
que.push(adjVet); // 未访问的顶点添加到队列里
}
}
for (unordered_set<Vertex*>::iterator it = visited.begin(); it != visited.end(); it++)
cout << *&(*it)->val << " ";
cout << endl;
}
return res;
}
/* 深度优先遍历 DFS 辅助函数 参数一是整个图,参数二是记录是否被访问过的顶点,第三个参数是顶点遍历的序列,第四个参数是当前访问的顶点*/
void dfs(GraphAdjList& graph, unordered_set<Vertex*>& visited, vector<Vertex*>& res, Vertex* vet) {
res.push_back(vet);//记录访问的点
visited.emplace(vet);//标记已被访问的顶点
//遍历当前顶点所有的邻接顶点
for (Vertex* adjVet : graph.adjList[vet]) {
//查询这个顶点是否已经被访问过,存在返回1,不存在返回0
if (visited.count(adjVet))continue;
// 递归访问邻接顶点
dfs(graph, visited, res, adjVet);
}
}
/* 深度优先遍历 DFS 参数一是整个图,参数二是开始递起点的顶点*/
vector<Vertex*> graphDFS(GraphAdjList& graph, Vertex* startVet) {
//顶点遍历的序列
vector<Vertex*>res;
//哈希表,用于记录已被访问过的顶点,值不重复
unordered_set<Vertex*>visited = { startVet };
dfs(graph, visited, res, startVet);
return res;
}
/******************************** 打印信息辅助函数 开始 *************************************************/
/* Concatenate a vector with a delim */
template <typename T> string strJoin(const string& delim, const T& vec) {
ostringstream s;
for (const auto& i : vec) {
if (&i != &vec[0]) {
s << delim;
}
s << i;
}
return s.str();
}
/* Get the Vector String object */
template <typename T> string getVectorString(vector<T>& list) {
return "[" + strJoin(", ", list) + "]";
}
/* Print a vector */
template <typename T> void printVector(vector<T> list) {
cout << getVectorString(list) << '\n';
}
/* 打印邻接表 */
void print() {
cout << "邻接表 =" << endl;
for (auto& adj : adjList) {
const auto& key = adj.first;
const auto& vec = adj.second;
cout << key->val << ": ";
printVector(vetsToVals(vec));
}
}
/******************************** 打印信息辅助函数 结束 *************************************************/
};
int main() {
/* 初始化无向图 */
vector<Vertex*> v = valsToVets(vector<int>{0, 1, 2, 3, 4, 5, 6});
vector<vector<Vertex*>> edges = { {v[0], v[1]}, {v[0], v[3]}, {v[1], v[2]},
{v[2], v[5]}, {v[4], v[5]}, {v[5], v[6]} };
GraphAdjList graph(edges);
cout << "\n初始化后,图为" << endl;
graph.print();
/* 深度优先遍历 DFS */
vector<Vertex*> res = graph.graphDFS(graph, v[0]);
cout << "\n深度优先遍历(DFS)顶点序列为" << endl;
graph.printVector(vetsToVals(res));
// 释放内存
for (Vertex* vet : v) {
delete vet;
}
return 0;
}
时间复杂度: 所有顶点都会被访问 1 次,使用 O(|V|) 时间;所有边都会被访问 2 次,使用 O(2|E|) 时间;总体使用 O(|V|+|E|) 时间。
空间复杂度: 列表 res
,哈希表 visited
顶点数量最多为 |V| ,递归深度最大为 |V| ,因此使用 O(|V|) 空间。
搜索
二分查找
「二分查找 binary search」是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。
先初始化指针 i=0 和 j=n−1 ,分别指向数组首元素和尾元素,代表搜索区间 [0,n−1] 。请注意,中括号表示闭区间,其包含边界值本身。
接下来,循环执行以下两步。
- 计算中点索引 m=⌊(i+j)/2⌋ ,其中 ⌊⌋ 表示向下取整操作。
- 判断 nums[m] 和 target 的大小关系,分如下三种情况。
-
- 当
nums[m] < target
时,说明target
在区间 [m+1,j] 中,因此执行 i=m+1 。
- 当
-
- 当
nums[m] > target
时,说明target
在区间 [i,m−1] 中,因此执行 j=m−1 。
- 当
-
- 当
nums[m] = target
时,说明找到target
,因此返回索引 m 。
- 当
若数组不包含目标元素,搜索区间最终会缩小为空。此时返回 −1 。
由于 i 和 j 都是 int
类型,因此 i+j 可能会超出 int
类型的取值范围。为了避免大数越界,通常采用公式 m=⌊i+(j−i)/2⌋ 来计算中点。
/* 二分查找 */
int binarySearch(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1;
while (i <= j) {
int mid = i + (j - i) / 2; //防止溢出
if (nums[mid] < target) i = mid + 1; //此情况说明 target 在区间 [mid+1, j] 中
else if (nums[mid] > target) j = mid - 1; //此情况说明 target 在区间 [i, mid-1] 中
else return mid; //找到target值,返回索引
}
//未找到target值,返回-1
return -1;
}
int main() {
//元素按从小到大的顺序排列,数组不包含重复元素
vector<int> nums = {1, 2, 3, 4, 5, 8, 9, 10};
int target = 10;
int index = binarySearch(nums, target);
cout << "target = " << target << " ,the target index = " << index << endl;
return 0;
}
间复杂度 O(logn) :在二分循环中,区间每轮缩小一半,循环次数为 logn。
空间复杂度 O(1) :指针 i 和 j 使用常数大小空间。
二分查找并非适用于所有情况,主要有以下原因。
- 二分查找仅适用于有序数据。若输入数据无序,为了使用二分查找而专门进行排序,得不偿失。
- 二分查找仅适用于数组。二分查找需要跳跃式(非连续地)访问元素,而在链表中执行跳跃式访问的效率较低,因此不适合应用在链表或基于链表实现的数据结构。
- 小数据量下,线性查找性能更佳。在线性查找中,每轮只需要 1 次判断操作;而在二分查找中,需要 1 次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,当数据量 n 较小时,线性查找反而比二分查找更快。
哈希优化策略
通过将线性查找替换为哈希查找来降低算法的时间复杂度。
Q:给定一个整数数组 nums 和一个目标元素 target ,请在数组中搜索“和”为 target 的两个元素,并返回它们的数组索引。返回任意一个解即可。
#include <bits/stdc++.h>
using namespace std;
/* 方法一:暴力枚举 */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// 两层循环,时间复杂度 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return {i, j};
}
}
return {};
}
int main() {
vector<int> nums = {1, 2, 3, 4, 5, 8, 9, 10};
int target = 10;
vector<int> sum_brute = twoSumBruteForce(nums,target);
cout << "sum_brute:nums index = " << sum_brute[0] << " ,nums index = " << sum_brute[1] << endl;
return 0;
}
时间复杂度为 O(n^2) ,空间复杂度为 O(1) ,在大数据量下非常耗时。
考虑借助一个哈希表,键值对分别为数组元素和元素索引。循环遍历数组。
-
- 判断数字
target - nums[i]
是否在哈希表中,若是则直接返回这两个元素的索引。
- 判断数字
-
- 将键值对
nums[i]
和索引i
添加进哈希表。
- 将键值对
#include <bits/stdc++.h>
using namespace std;
/* 方法二:辅助哈希表 */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 辅助哈希表,空间复杂度 O(n) <元素,索引>
unordered_map<int, int> dic;
// 单层循环,时间复杂度 O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
for(auto m : dic){
cout << "key = " << m.first << " ,val = " << m.second << endl;
}
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
int main() {
vector<int> nums = {1, 2, 3, 4, 5, 8, 9, 10};
int target = 10;
vector<int> sum_hash = twoSumHashTable(nums,target);
cout << "sum_hash: map val = " << sum_hash[0] << " ,nums index = " << sum_hash[1] << endl;
return 0;
}
查找算法效率
线性搜索 | 二分查找 | 树查找 | 哈希查找 | |
---|---|---|---|---|
查找元素 | O(n) | O(logn) | O(logn) | O(1) |
插入元素 | O(1) | O(n) | O(logn) | O(1) |
删除元素 | O(n) | O(n) | O(logn) | O(1) |
额外空间 | O(1) | O(1) | O(n) | O(n) |
数据预处理 | / | 排序O(nlogn) | 建树O(nlogn) | 建哈希表O(n) |
数据是否有序 | 无序 | 有序 | 有序 | 无序 |
排序算法
选择排序
「选择排序 selection sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。
设数组的长度为 n 。
-
- 初始状态下,所有元素未排序,即未排序(索引)区间为 [0,n−1] 。
-
- 选取区间 [0,n−1] 中的最小元素,将其与索引 0 处元素交换。完成后,数组前 1 个元素已排序。
-
- 选取区间 [1,n−1] 中的最小元素,将其与索引 1 处元素交换。完成后,数组前 2 个元素已排序。
-
- 以此类推。经过 n−1 轮选择与交换后,数组前 n−1 个元素已排序。
-
- 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。
#include <bits/stdc++.h>
using namespace std;
/* 排序算法 */
/* 选择排序 从小到大排序*/
void selectionSort(vector<int> nums) {
int n = nums.size();
// 外循环:未排序区间为 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 内循环:找到未排序区间内的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 记录最小元素的索引
}
// 将该最小元素与未排序区间的首个元素交换
swap(nums[i], nums[k]);
}
for (int v: nums)cout << v << " ";
cout << endl;
}
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 选择排序 从小到大排序*/
selectionSort(nums);
return 0;
}
- 时间复杂度为 O(n^2)、非自适应排序
- 空间复杂度 O(1)、原地排序:指针 i 和 j 使用常数大小的额外空间。
冒泡排序
「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。
冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
设数组的长度为 n。
-
- 首先,对 n 个元素执行“冒泡”,将数组的最大元素交换至正确位置,
-
- 接下来,对剩余 n−1 个元素执行“冒泡”,将第二大元素交换至正确位置。
-
- 以此类推,经过 n−1 轮“冒泡”后,前 n−1 大的元素都被交换至正确位置。
-
- 仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成。
#include <bits/stdc++.h>
using namespace std;
/* 排序算法 */
/* 冒泡排序 从小到大排序*/
void bubbleSort(vector<int> nums) {
// 外循环:未排序区间为 [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交换 nums[j] 与 nums[j + 1]
// 这里使用了 std::swap() 函数
swap(nums[j], nums[j + 1]);
}
}
}
for (int v: nums)cout << v << " ";
cout << endl;
}
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 冒泡排序 从小到大排序*/
bubbleSort(nums);
return 0;
}
- 时间复杂度为 O(n^2)、自适应排序
- 空间复杂度为 O(1)、原地排序:指针 i 和 j 使用常数大小的额外空间。
- 稳定排序:由于在“冒泡”中遇到相等元素不交换。
插入排序
「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。
具体来说,在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
插入排序的整体流程。
- 初始状态下,数组的第 1 个元素已完成排序。
- 选取数组的第 2 个元素作为
base
,将其插入到正确位置后,数组的前 2 个元素已排序。 - 选取第 3 个元素作为
base
,将其插入到正确位置后,数组的前 3 个元素已排序。 - 以此类推,在最后一轮中,选取最后一个元素作为
base
,将其插入到正确位置后,所有元素均已排序。
#include <bits/stdc++.h>
using namespace std;
/* 排序算法 */
/* 插入排序 从小到大排序*/
void insertionSort(vector<int> nums) {
// 外循环:已排序元素数量为 1, 2, ..., n
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// 内循环:将 base 插入到已排序部分的正确位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
j--;
}
nums[j + 1] = base; // 将 base 赋值到正确位置
}
for (int v: nums)cout << v << " ";
cout << endl;
}
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 插入排序 从小到大排序*/
insertionSort(nums);
return 0;
}
- 时间复杂度 O(n^2)、自适应排序
- 空间复杂度 O(1)、原地排序:指针 i 和 j 使用常数大小的额外空间。
- 稳定排序:在插入操作过程中,会将元素插入到相等元素的右侧,不会改变它们的顺序。
快速排序
「快速排序 quick sort」是一种基于分治策略的排序算法,运行高效,应用广泛。
快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。【找:右小左大,然后交换值,成右大左小】
-
- 选取数组最左端元素作为基准数,初始化两个指针
i
和j
分别指向数组的两端。
- 选取数组最左端元素作为基准数,初始化两个指针
-
- 设置一个循环,在每轮中使用
i
(j
)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
- 设置一个循环,在每轮中使用
-
- 循环执行步骤
2.
,直到i
和j
相遇时停止,最后将基准数交换至两个子数组的分界线,i = j时,返回基准数索引。
- 循环执行步骤
哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 ≤ 基准数 ≤ 右子数组任意元素”。
哨兵划分的实质是将一个较长数组的排序问题简化为两个较短数组的排序问题。
快速排序的整体流程。
-
- 首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。
-
- 然后,对左子数组和右子数组分别递归执行“哨兵划分”。
-
- 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。
#include <bits/stdc++.h>
using namespace std;
/* 排序算法 */
/* 快速排序 从小到大排序 开始 */
/* 元素交换 */
void swap(vector<int> &nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 哨兵划分 */
int partition(vector<int> &nums, int left, int right) {
// 以 nums[left] 作为基准数
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素,就跳出该循环
while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素,就跳出该循环
swap(nums, i, j); // 交换这两个元素
}
swap(nums, i, left); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
/* 快速排序 从小到大排序*/
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止递归
if (left >= right) return;
// 哨兵划分
int pivot = partition(nums, left, right);
quickSort(nums, left, pivot - 1);// 递归左子数组
quickSort(nums, pivot + 1, right);// 递归右子数组
}
/* 快速排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 快速排序 从小到大排序*/
quickSort(nums, 0, nums.size() - 1);
for (int v: nums)cout << v << " ";
cout << endl;
return 0;
}
- 时间复杂度 O(nlogn)、自适应排序:在平均情况下,哨兵划分的递归层数为 logn ,每层中的总循环数为 n ,总体使用 (nlogn) 时间。在最差情况下,每轮哨兵划分操作都将长度为 n 的数组划分为长度为 0 和 n−1 的两个子数组,此时递归层数达到 n 层,每层中的循环数为 n ,总体使用 O(n^2) 时间。
- 空间复杂度 O(n)、原地排序:在输入数组完全倒序的情况下,达到最差递归深度 n ,使用 O(n) 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。
- 非稳定排序:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。
基准数优化
可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),并将这三个候选元素的中位数作为基准数。
尾递归优化
在某些输入下,快速排序可能占用空间较多。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,递归树的高度会达到 n−1 ,此时需要占用 O(n) 大小的栈帧空间。
为了防止栈帧空间的累积,可以在每轮哨兵排序完成后,比较两个子数组的长度,仅对较短的子数组进行递归。由于较短子数组的长度不会超过 n/2 ,因此这种方法能确保递归深度不超过 logn ,从而将最差空间复杂度优化至 O(logn) 。
#include <bits/stdc++.h>
using namespace std;
/* 优化快速排序 从小到大排序 开始 */
void swap(vector<int> &nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 选取三个元素的中位数 */
int medianThree(vector<int> &nums, int left, int mid, int right) {
// 此处使用异或运算来简化代码
// 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1
if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right]))
return left;
else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right]))
return mid;
else
return right;
}
/* 哨兵划分(三数取中值) */
int partition(vector<int> &nums, int left, int right) {
// 选取三个候选元素的中位数
int med = medianThree(nums, left, (left + right) / 2, right);
// 将中位数交换至数组最左端
swap(nums, left, med);
// 以 nums[left] 作为基准数
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素
while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素
swap(nums, i, j); // 交换这两个元素
}
swap(nums, i, left); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
/* 快速排序(尾递归优化) 从小到大排序 */
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
// 哨兵划分操作
int pivot = partition(nums, left, right);
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
}
}
}
/* 优化快速排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 优化快速排序 从小到大排序*/
quickSort(nums, 0, nums.size() - 1);
for (int v: nums)cout << v << " ";
cout << endl;
return 0;
}
归并排序
「归并排序 merge sort」是一种基于分治策略的排序算法,包含“划分”和“合并”阶段。
-
- 划分阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。
-
- 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。
“划分阶段”从顶至底递归地将数组从中点切分为两个子数组。
- 计算数组中点
mid
,递归划分左子数组(区间[left, mid]
)和右子数组(区间[mid + 1, right]
)。 - 递归执行步骤
1.
,直至子数组区间长度为 1 时,终止递归划分。
**“合并阶段”从底至顶地将左子数组和右子数组合并为一个有序数组。**需要注意的是,从长度为 1 的子数组开始合并,合并阶段中的每个子数组都是有序的。
归并排序与二叉树后序遍历的递归顺序是一致的。
- 后序遍历:先递归左子树,再递归右子树,最后处理根节点。
- 归并排序:先递归左子数组,再递归右子数组,最后处理合并。
#include <bits/stdc++.h>
using namespace std;
/* 归并排序 从小到大排序 开始 */
// nums 的待合并区间为 [left, right] ,但由于 tmp 仅复制了 nums 该区间的元素,因此 tmp 对应区间为 [0, right - left]
/* 合并左子数组和右子数组 */
// 左子数组区间 [left, mid]
// 右子数组区间 [mid + 1, right]
void merge(vector<int> &nums, int left, int mid, int right) {
// 初始化辅助数组 定义是 [left, right) 左闭右开的,因此 right 需要加 1
vector<int> tmp(nums.begin() + left, nums.begin() + right + 1);
// 左子数组的起始索引和结束索引
int leftStart = left - left, leftEnd = mid - left;
// 右子数组的起始索引和结束索引
int rightStart = mid + 1 - left, rightEnd = right - left;
// i, j 分别指向左子数组、右子数组的首元素
int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) {
// 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd)
nums[k] = tmp[j++];
// 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++];
// 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else
nums[k] = tmp[j++];
}
}
/* 归并排序 从小到大排序*/
void mergeSort(vector<int> &nums, int left, int right) {
// 终止条件
if (left >= right)
return; // 当子数组长度为 1 时终止递归
// 划分阶段
int mid = (left + right) / 2; // 计算中点
mergeSort(nums, left, mid); // 递归左子数组
mergeSort(nums, mid + 1, right); // 递归右子数组
// 合并阶段
merge(nums, left, mid, right);
}
/* 归并排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 归并排序 从小到大排序*/
mergeSort(nums, 0, nums.size() - 1);
for (int v: nums)cout << v << " ";
cout << endl;
return 0;
}
- 时间复杂度 O(nlogn)、非自适应排序:划分产生高度为 logn 的递归树,每层合并的总操作数量为 n ,因此总体时间复杂度为 O(nlogn) 。
- 空间复杂度 O(n)、非原地排序:递归深度为 logn ,使用 O(logn) 大小的栈帧空间。合并操作需要借助辅助数组实现,使用 O(n) 大小的额外空间。
- 稳定排序:在合并过程中,相等元素的次序保持不变。
堆排序
「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。
利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。
- 输入数组并建立小顶堆,此时最小元素位于堆顶。
- 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。
设数组的长度为 n ,堆排序的流程如下。
输入数组并建立大顶堆。完成后,最大元素位于堆顶。
- 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 1 ,已排序元素数量加 1 。
- 从堆顶元素开始,从顶到底执行堆化操作(Sift Down)。完成堆化后,堆的性质得到修复。
- 循环执行第
2.
和3.
步。循环 n−1 轮后,即可完成数组排序。
#include <bits/stdc++.h>
using namespace std;
/* 堆排序 从小到大排序 开始 */
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 建大顶堆操作*/
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// 判断节点 i, l, r 中值最大的节点,记为 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if (ma == i) {
break;
}
// 交换两节点
swap(nums[i], nums[ma]);
// 循环向下堆化
i = ma;
}
}
/* 堆排序 从小到大排序*/
void heapSort(vector<int> nums) {
// 建大顶堆操作:堆化除叶节点以外的其他所有节点,最后一个元素的父节点(i-1)/2
// i = nums.size()-1,故,i = (nums.size()-1-1)/2 = nums.size()/2 -1
for (int i = nums.size() / 2 - 1; i >= 0; --i) {
siftDown(nums, nums.size(), i);
}
// 从堆中提取最大元素,循环 n-1 轮
for (int i = nums.size() - 1; i > 0; --i) {
// 交换根节点与最右叶节点(即交换首元素与尾元素)
swap(nums[0], nums[i]);
// 以根节点为起点,从顶至底进行堆化
siftDown(nums, i, 0);
}
for (int v: nums)cout << v << " ";
cout << endl;
}
/* 堆排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 堆排序 从小到大排序*/
heapSort(nums);
return 0;
}
- 时间复杂度 O(nlogn)、非自适应排序:建堆操作使用 O(n) 时间。从堆中提取最大元素的时间复杂度为 O(logn) ,共循环 n−1 轮。
- 空间复杂度 O(1)、原地排序:几个指针变量使用 O(1) 空间。元素交换和堆化操作都是在原数组上进行的。
- 非稳定排序:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。
桶排序
「桶排序 bucket sort」是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。
设长度为 n 的数组,元素是范围 [0,1) 的浮点数。桶排序的流程如下。
- 初始化 k 个桶,将 n 个元素分配到 k 个桶中。
- 对每个桶分别执行排序(本文采用编程语言的内置排序函数)。
- 按照桶的从小到大的顺序,合并结果。
#include <bits/stdc++.h>
using namespace std;
/* 桶排序 从小到大排序 开始 */
void bucketSort(vector<int>& nums) {
auto minmax = minmax_element(nums.begin(), nums.end());
int min_value = *minmax.first;
int max_value = *minmax.second;
// 确定 桶的个数
int k = (max_value - min_value ) / nums.size() + 1; // 修改1
vector<vector<int>> buckets(k);
// 1. 将数组元素分配到各个桶中
for (int num : nums) {
int i = (num - min_value)/ nums.size(); // 修改2
// 将 num 添加进桶 bucket_idx
buckets[i].push_back(num);
}
// 2. 对各个桶执行排序
for (vector<int>& bucket : buckets) {
// 使用内置排序函数,也可以替换成其他排序算法
sort(bucket.begin(), bucket.end());
}
// 3. 遍历桶合并结果
int i = 0;
for (vector<int>& bucket : buckets) {
for (int num : bucket) {
nums[i++] = num;
}
}
}
/* 桶排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 桶排序 从小到大排序*/
bucketSort(nums);
for (int v: nums)cout << v << " ";
cout << endl;
return 0;
}
桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。
- 时间复杂度 O(n+k) :当桶数量 k 比较大时,时间复杂度则趋向于 O(n) 。合并结果时需要遍历所有桶和元素,花费 O(n+k) 时间。
- 自适应排序:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 O(n^2) 时间。
- 空间复杂度 O(n+k)、非原地排序:需要借助 k 个桶和总共 n 个元素的额外空间。
- 桶排序是否稳定取决于排序桶内元素的算法是否稳定。
计数排序
「计数排序 counting sort」通过统计元素数量来实现排序,通常应用于整数数组。
给定一个长度为 n 的数组 nums
,其中的元素都是“非负整数”,计数排序的整体流程如下。
- 遍历数组,找出数组中的最大数字,记为 m ,然后创建一个长度为 m+1 的辅助数组
counter
。 - 借助
counter
统计nums
中各数字的出现次数,其中counter[num]
对应数字num
的出现次数。统计方法很简单,只需遍历nums
(设当前数字为num
),每轮将counter[num]
增加 1 即可。 - 由于
counter
的各个索引天然有序,因此相当于所有数字已经被排序好了。接下来,遍历counter
,根据各数字的出现次数,将它们按从小到大的顺序填入nums
即可。
首先计算 counter
的“前缀和”。顾名思义,索引 i
处的前缀和 prefix[i]
等于数组前 i
个元素之和:
p
r
e
f
i
x
[
i
]
=
∑
j
=
0
i
c
o
u
n
t
e
r
[
j
]
prefix[i]=\sum_{j=0}^i counter[j]
prefix[i]=j=0∑icounter[j]
前缀和具有明确的意义,prefix[num] - 1
代表元素 num
在结果数组 res
中最后一次出现的索引。这个信息非常关键,说明了各个元素应该出现在结果数组的哪个位置。接下来,倒序遍历原数组 nums
的每个元素 num
,在每轮迭代中执行以下两步。
- 将
num
填入数组res
的索引prefix[num] - 1
处。 - 令前缀和
prefix[num]
减小 1 ,从而得到下次放置num
的索引。
遍历完成后,数组 res
中就是排序好的结果,最后使用 res
覆盖原数组 nums
即可。
#include <bits/stdc++.h>
using namespace std;
/* 计数排序 从小到大排序 开始 */
void countingSort(vector<int> nums) {
// 1. 统计数组最大元素Max和最小元素Min
int n = nums.size(), Min = nums[0], Max = nums[0];
for(int num : nums) { // 确定 Min 和 Max
Min = min(Min,num);
Max = max(Max,num);
}
// 2. 统计各数字的出现次数
// counter[num - Min] 代表 num 的出现次数
vector<int> counter(Max - Min + 1,0); //nums最多有Max-Min+1种数字,全部初始化为0;
for(int num: nums) {
counter[num - Min] ++; // nums[i] 的值出现一次,则 countArr[nums[i]-min] 加 1
}
// 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”
for(int i = 1; i < counter.size(); ++i) {
counter[i] += counter[i-1];
}
// 4. 倒序遍历 nums ,将各元素填入结果数组 res
// 初始化数组 res 用于记录结果
vector<int> res(n);
for(int i = n - 1; i >= 0; -- i) {
int countIdx = nums[i] - Min; // nums[i] 元素对应 counter 中的下标
int sortIdx = counter[countIdx] - 1; // 在排序后数组中的下标
res[sortIdx] = nums[i]; // 在排序后数组中填入值
counter[countIdx]--; // 令前缀和自减 1 ,方便得到下次放置 num 的索引
}
// 使用结果数组 res 覆盖原数组 nums
nums = res;
for (int v: nums)cout << v << " ";
cout << endl;
}
/* 计数排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 计数排序 从小到大排序 */
countingSort(nums);
return 0;
}
- 时间复杂度 O(n+m) :涉及遍历
nums
和遍历counter
,都使用线性时间。一般情况下 n≫m ,时间复杂度趋于 O(n) 。 - 空间复杂度 O(n+m)、非原地排序:借助了长度分别为 n 和 m 的数组
res
和counter
。 - 稳定排序:由于向
res
中填充元素的顺序是“从右向左”的,因此倒序遍历nums
可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历nums
也可以得到正确的排序结果,但结果是非稳定的。
基数排序
「基数排序 radix sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。由于数字的高位优先级高于低位,应该先排序低位再排序高位。
对于一个 d 进制的数字 x ,要获取其第 k 位数值 ,可以使用以下计算公式:
Xk= ⌊x/(d^(k-1))⌋ mod d
这里数据,d=10 且 k∈[1,8] 。
#include <bits/stdc++.h>
using namespace std;
/* 基数排序 从小到大排序 开始 */
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */
int digit(int num, int exp) {
// 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算
return (num / exp) % 10;
}
/* 计数排序(根据 nums 第 k 位排序) */
void countingSortDigit(vector<int> &nums, int exp) {
// 十进制的位范围为 0~9 ,因此需要长度为 10 的桶
vector<int> counter(10, 0);
int n = nums.size();
// 统计 0~9 各数字的出现次数
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d
counter[d]++; // 统计数字 d 的出现次数
}
// 求前缀和,将“出现个数”转换为“数组索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序遍历,根据桶内统计结果,将各元素填入 res
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 获取 d 在数组中的索引 j
res[j] = nums[i]; // 将当前元素填入索引 j
counter[d]--; // 将 d 的数量减 1
}
// 使用结果覆盖原数组 nums
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数排序 从小到大排序*/
void radixSort(vector<int> nums) {
// 获取数组的最大元素,用于判断最大位数
int m = *max_element(nums.begin(), nums.end());
// 按照从低位到高位的顺序遍历
for (int exp = 1; exp <= m; exp *= 10) {
// 对数组元素的第 k 位执行计数排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
for (int v: nums)cout << v << " ";
cout << endl;
}
/* 基数排序 从小到大排序 结束 */
int main() {
//无序数组
vector<int> nums = {2, 9, 3, 6, 1, 8, 7, 5, 4};
/* 基数排序 从小到大排序*/
radixSort(nums);
return 0;
}
相较于计数排序,基数排序适用于数值范围较大的情况,但前提是数据必须可以表示为固定位数的格式,且位数不能过大。例如,浮点数不适合使用基数排序,因为其位数 k 过大,可能导致时间复杂度 O(nk)≫O(n^2) 。
- 时间复杂度 O(nk):设数据量为 n、数据为 d 进制、最大位数为 k ,则对某一位执行计数排序使用 O(n+d) 时间,排序所有 k 位使用 O((n+d)k) 时间。通常情况下,d 和 k 都相对较小,时间复杂度趋向 O(n) 。
- 空间复杂度 O(n+d)、非原地排序:与计数排序相同,基数排序需要借助长度为 n 和 d 的数组
res
和counter
。 - 稳定排序:与计数排序相同。