文章目录
- 前言
- I、 数组部分
- 数组部分总结
- II 、链表部分
- 链表部分总结:
- Ⅲ、哈希表部分
- 哈希表部分总结:
- Ⅳ、字符串部分
- Ⅴ、栈和队列部分
- Ⅵ、二叉树
- Ⅶ、回溯算法
- 动态规划
前言
对代码随想录中的一些算法题进行总结
I、 数组部分
一、27移除元素(简单)
解题思路:
数组中的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
1. 暴力解法:
使用两层for循环,一个遍历数组,另一个更新数组
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
2. 双指针(快慢指针)
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
// 双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != val) { // 不是要移除的元素,就将慢指针往后移
// nums[slowIndex] = nums[fastIndex];
// slowIndex ++;
nums[slowIndex++] = nums[fastIndex];
}
}
return size;
}
};
二、977有序数组的平方(简单)
解题思路:
返回一个新数组,就类似于双指针,一个进行遍历一个进行数组的更新
false
1. 暴力解法
每个数平方之后,排个序
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i=0; i<nums.size(); i++) {
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end()); // 快速排序
return nums;
}
};
2. 双指针法(两侧)
1. 解题思路存在错误,双指针并不会开辟一个新数组,而是在原数组中进行更新; 2. 新数组元素更新时,需要定义一个新的指针。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int k = nums.size() - 1;
int leftIndex = 0;
int rightIndex = nums.size() - 1;
vector<int> result(nums.size(), 0);
while (leftIndex <= rightIndex) {
if (nums[leftIndex]*nums[leftIndex] > nums[rightIndex]*nums[rightIndex]) {
result[k --] = nums[leftIndex] * nums[leftIndex];
leftIndex ++;
}
else {
result[k --] = nums[rightIndex] * nums[rightIndex];
rightIndex --;
}
}
return result;
}
};
三、209长度最小的子数组(中等)
解题思路:
类似于双指针(快慢指针),指针之间的累加和大于等于目标值时,更新慢指针
1.滑动窗口
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int subLength = 0;
int sum = 0;
// 最大值
int result = INT32_MAX;
// 慢指针
int i = 0;
// 快指针遍历
for (int j=0; j<nums.size(); j++) {
sum += nums[j];
while (sum >= target) {
// 子数组长度
subLength = (j-i+1);
// 比较长度,更新result
result = result < subLength ? result : subLength;
// 慢指针更新
sum -= nums[i ++];
}
}
return result = result == INT32_MAX ? 0 : result;
}
};
类似题目补充:(不会做系列)
1. 904水果成篮(中等)
2. 76最小覆盖子串(困难)
四、59螺旋矩阵 II(中等)
解题思路:
关键是循环不变量,即边界条件
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 从左到右(左闭右开)
for (j=starty; j < n - offset; j++) {
res[startx][j] = count ++;
}
// 从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count ++;
}
// 从右往左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count ++;
}
// 从下往上
for (; i > startx; i--) {
res[i][j] = count ++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
类似题目补充:
1. 54螺旋矩阵(中等)
解题思路
循环进行读取,控制循环不变量; 主要是最后的中间区域需要进行判定
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0)
return {};
int k = 0;
// 矩阵的行列
int m = matrix.size();
int n = matrix[0].size();
// 使用vector定义一个数组
vector<int> res(m*n);
// 循环次数
int loop = min(m, n) / 2;
// 定义起始位置
int startx = 0;
int starty = 0;
// 矩阵中间位置
int mid = min(m, n) / 2;
// 偏移量
int offset = 1;
int i, j;
// 循环读取
while (loop --) {
i = startx;
j = starty;
// 上边,从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[k ++] = matrix[startx][j];
}
// 右边,从上到下(左闭右开)
for (i = startx; i < m - offset; i++) {
res[k ++] = matrix[i][j];
}
// 下边,从右到左(左闭右开)
for (; j > starty; j--) {
res[k ++] = matrix[i][j];
}
// 左边,从下到上(左闭右开)
for (; i > startx; i--) {
res[k ++] = matrix[i][starty];
}
// 第二轮循环
startx ++;
starty ++;
offset ++;
}
// 如果min(rows, columns)为奇数的话,需要单独给矩阵最中间的位置赋值
if (min(m, n) % 2) {
if(m > n){
for (int i = mid; i < mid + m - n + 1; ++i) {
res[k++] = matrix[i][mid];
}
} else {
for (int i = mid; i < mid + n - m + 1; ++i) {
res[k++] = matrix[mid][i];
}
}
}
return res;
}
};
五、704 二分查找(简单)
解题思路:
因为是有序数组,所以与区间中间值进行比较
1. 控制循环不变量(左闭右闭)
// 左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
int begin = 0;
// 边界条件
int end = nums.size() - 1;
while (begin <= end) {
int middle = (begin + end) / 2;
if (nums[middle] > target) {
end = middle - 1;
} else if (nums[middle] < target) {
begin = middle + 1;
} else {
return middle;
}
}
return -1;
}
};
2.控制循环不变量(左闭右开)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
// 边界条件
int right = nums.size();
while (left <right) {
int middle = left + (right - left) / 2;
if (nums[middle] > target) {
right = middle;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
};
数组部分总结
II 、链表部分
链表的概念:
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的类型:
- 单链表;
- 双链表;
- 循环链表;
链表的存储方式:
数组在内存中是连续分布的,但是链表在内存空间中不是连续分布;
链表通过指针域的指针链接在内存中各个节点,所以链表中的节点不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存分配
链表的定义:
// 单链表 struct ListNode { int val; // 节点上存储的元素 ListNode *next; // 指向下一个节点的指针 ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 };
链表的操作
1. 删除节点
2. 添加节点
性能分析
一、203移除链表元素(简单)
解题思路:
1. 设置一个虚拟头节点再进行移除节点操作; 2. 直接在原链表的基础上进行移除(头节点需要单独判断处理)。
注意:c/c++需要对移除的节点进行删除。
1. 原链表直接进行移除节点操作
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) {
ListNode* tmp = head; // 头节点
head = head->next; // 头节点直接下一个节点
delete tmp; // 删除移除的节点
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next != NULL) {
// 如果下一节点的值为目标值,再指向下一节点
if (cur->next->val == val) {
ListNode* tmp = cur->next; // 下一节点
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};
2. 设置一个虚拟头节点再进行移除节点操作
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
ListNode* cur = dummyHead;
while (cur->next != NULL) {
// 判断下一节点的值是否为目标值
if(cur->next->val == val) {
// 释放删除的结点
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
head = dummyHead->next; // 删除虚拟节点
delete dummyHead;
return head;
}
};
二、707设计链表(中等)–比较考察链表基础
解题思路:
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
三、206反转链表(简单)
解题思路:
第一想法:将指针的指向翻转一下
1. 双指针
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = NULL;
ListNode* cur = head;
ListNode* tmp;
while (cur) {
tmp = cur->next;
cur->next = pre;
// 更新指针
pre = cur;
cur = tmp;
}
return pre;
}
};
2.递归法(和双指针逻辑一致)
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
四、24两两交换链表中的节点(中等)
解题思路:
1. 指针
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 定义一个虚拟头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
while (cur->next != nullptr && cur->next->next != nullptr) {
ListNode* tmp = cur->next;
ListNode* tmp1 = cur->next->next->next;
// 虚拟头节点指向
cur->next = cur->next->next; // 步骤一
cur->next->next = tmp; // 步骤二
cur->next->next->next = tmp1; // 步骤三
// 更新指针
cur = cur->next->next;
}
return dummyHead->next;
}
};
五、删除链表的倒数第N个节点(中等)
解题思路:
双指针(快慢指针):创建一个虚拟头结点,两个指针之间相距N+1个节点,快指针指向NULL之后,慢指针所指向的便是所要删除的结点
1.快慢指针
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//创建虚拟头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
// 定义快慢指针
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
// 快指针移动n+1次
while (n -- && fast!=nullptr) {
fast = fast->next;
}
fast = fast->next;
// 同时往后移动
while (fast!=nullptr) {
slow = slow->next;
fast = fast->next;
}
//删除节点
slow->next = slow->next->next;
return dummyHead->next;
}
};
六、面试题-02.07链表相交(简单)
解题思路:
注意:交点不是数值相等,而是指针相等,即指针指向的地址相等。(不用纠结示例中为什么1不相等)
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--) {
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
七、142环形链表II(中等)
解题思路:
第一次思路为使用快慢指针进行两层循环
1.哈希表
遍历链表中的每个节点,并将它记录下来;一旦遇到此前遍历过的节点,就可以判定链表中存在环。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 定义哈希表
unordered_set<ListNode *> visited;
while (head != nullptr) {
// 统计某个节点是否重复
if (visited.count(head)) {
return head;
}
visited.insert(head);
head = head->next;
}
return nullptr;
}
};
2.快慢指针
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 创建快慢指针
ListNode* fast = head;
ListNode* slow = head;
// 快指针移动两个节点,慢指针移动一个
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
链表部分总结:
Ⅲ、哈希表部分
哈希表是根据关键码的值而直接进行访问的数据结构
哈希表能解决什么问题呢? 一般哈希表都是用来快速判断一个元素是否出现在集合里
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来 哈希碰撞 登场
哈希碰撞
1. 拉链法:
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
2. 线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
常见的三种哈希结构
- 数组
- set (集合)
- map (映射)
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
一、242有效的字母异位词(简单)
解题思路:
定义数组记录每个字符出现的次数,需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
record[s[i] - 'a'] ++;
}
for (int j=0; j<t.size(); j++) {
record[t[j] - 'a'] --;
}
for (int i=0; i<26; i++) {
if (record[i] != 0 ) {
return false;
}
}
return true;
}
};
二、349两个数组的交集(简单)
解题思路:
题目提出 “输出结果中的每个元素一定是唯一的且不考虑输出结果的顺序” ,所以考虑使用 unordered_set 记录结果 。 解法一:1. 将其中某个数组转表为哈希表;2.零一数组与哈希表进行对比 ;解法二:题目补充了数组最大值小于等于1000,创建一个数组统计其中某个数组是否出现,然后遍历另一数组。
解法1:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
// 存放输出结果
unordered_set<int> result_set;
unordered_set<int> num_set(nums1.begin(), nums1.end());
// 遍历nums2
for (int num : nums2) {
if (num_set.find(num) != num_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
解法2:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
// 创建一个数组,用于统计出现的数字
int record[1000] = {0};
for (int num : nums1) {
record[num] = 1;
}
for (int num :nums2) {
if (record[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
三、202快乐数(简单)
解题思路:
取余->平方和->判断(使用哈希表处理 无限循环)
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
四、1两数之和(简单)
解题思路:
两层循环对数组进行遍历,如果两数相加等于target且下标不相等,就添加输出
1.两层循环(暴力解法)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 数组中同一元素在答案里不能重复出现
unordered_set<int> result;
for (int i=0; i< nums.size(); i++) {
for (int j=i; j<nums.size(); j++) {
if (nums[i] + nums[j] == target) {
if (i != j) {
result.insert(i);
result.insert(j);
}
}
}
}
return vector<int>(result.begin(), result.end());
}
};
2. 哈希表
// 时间复杂度O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
// auto iter 返回一个迭代器
auto iter = map.find(target - nums[i]);
// 如果找到了,返回一个包含两个索引的vector,其中第一个是iter指向的键的值,第二个是当前元素的索引i。
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
五、454四数相加II(中等)
解题思路:
1.首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来
5. 最后返回统计值 count 就可以了
哈希表(Need-Improve)
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
六、383赎金信(简单)
解题思路:
统计字符出现的次数,和前面的242有效的字母异位词很像
1.数组统计字符出现次数
// 时间复杂度O(n)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
// 定义哈希表统计每个字符出现的次数
int record[26] = {0};
if (ransomNote.length() > magazine.length()) {
return false;
}
// 统计字符出现次数
for (int i=0; i<magazine.length(); i++) {
record[magazine[i] - 'a'] ++;
}
// 字符出现次数--,小于0则false
for (int i=0; i< ransomNote.length(); i++) {
record[ransomNote[i] - 'a'] --;
if(record[ransomNote[i]-'a'] < 0) {
return false;
}
}
return true;
}
};
2.暴力解法:
出现对应字符就消除,思想相同
// 时间复杂度O(n^2)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for (int i = 0; i < magazine.length(); i++) {
for (int j = 0; j < ransomNote.length(); j++) {
// 在ransomNote中找到和magazine相同的字符
if (magazine[i] == ransomNote[j]) {
ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
break;
}
}
}
// 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
if (ransomNote.length() == 0) {
return true;
}
return false;
}
};
七、15三数之和(中等)
解题思路:
哈希表,使用两层循环确定两个数,然后使用哈希法来确定0-(a+b)是否在数组里出现过,但是题目要求不能包含重复的三元组
重点是剪枝和去重!!!
1. 哈希表(去重比较麻烦)
// 时间复杂度 O(n^2)
// 空间复杂度 O(n),额外的set开销
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
// 数组进行排序
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
// 如果遍历的位置i,前面出现过该值,说明存在包含该值的三元组
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
// 去重
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c); // 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
2. 双指针
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
八、四数之和(中等)
解题思路:
第一想法:哈希加指针,先固定一个数值,然后就是求三数之和。
1. 双指针
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
// 1.先将数组进行排序
sort(nums.begin(), nums.end());
for (int i=0; i<nums.size(); i++) {
// 剪枝处理
if (nums[i] > target && nums[i] >= 0 ) {
break;
}
// 去重nums[a]
if (i > 0 && nums[i] == nums[i-1]) {
continue;
}
for (int j=i+1; j<nums.size(); j++) {
// 2级剪枝
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) {
break;
}
// 去重nums[b]
if (j > i + 1 && nums[j] == nums[j-1]) {
continue;
}
int left = j+1;
int right = nums.size() - 1;
while (right > left) {
// nums[i] + nums[j] + nums[left] + nums[right] > target 会溢出
if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right --;
}
// nums[i] + nums[j] + nums[left] + nums[right] < target 会溢出
else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left ++;
}
else {
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};
哈希表部分总结:
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用.
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
- 数组的使用:
- 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
- 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。;
- set的使用:
- 使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
- map的使用:
- 使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。
Ⅳ、字符串部分
一、反转字符串(简单)
解题思路:
第一眼思路,没有想到两两交换
1. 双指针
// 时间复杂度 O(n)
// 空间复杂度 O(1)
class Solution {
public:
void reverseString(vector<char>& s) {
for (int i=0, j=s.size()-1; i<s.size()/2; i++, j--) {
swap(s[i], s[j]);
}
}
};
二、反转字符串Ⅱ(简单)
解题思路:
第一想法:统计字符数,然后进行判断,最后进行反转(比较繁琐)
只需要遍历时,每次移动2k
// 时间复杂度 O(n)
class Solution {
public:
string reverseStr(string s, int k) {
for (int i=0; i<s.size(); i+=(2*k)) {
// 如果剩余字符小于2k但大于或等于k个,则反转前k个字符
if (i+k <= s.size()) {
reverse(s.begin()+i, s.begin()+i+k);
} else {
// 剩余字符少于k个,全部反转
reverse(s.begin()+i, s.end());
}
}
return s;
}
};
三、剑指offer05替换空格(简单)
解题思路:
第一想法:进行遍历,然后判断是否为空格,然后进行处理
1. 暴力解法:
// 时间复杂度 O(n)
class Solution {
public:
string replaceSpace(string s) {
int ret = s.find(" ");
if (ret == -1) {
return s;
} else {
for (int i=ret; i<s.size(); i++) {
if (s[i] == ' ') {
s.replace(i, 1, "%20");
}
}
}
return s;
}
};
2. 双指针:
首先扩充数组到每个空格替换成“%20”之后的大小,然后从后往前替换空格
// 时间复杂度 O(n)
class Solution {
public:
string replaceSpace(string s) {
int count = 0; // 统计空格的个数
int sOldSize = s.size();
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') {
count++;
}
}
// 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
s.resize(s.size() + count * 2);
int sNewSize = s.size();
// 从后先前将空格替换为"%20"
for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) {
if (s[j] != ' ') {
s[i] = s[j];
} else {
s[i] = '0';
s[i - 1] = '2';
s[i - 2] = '%';
i -= 2;
}
}
return s;
}
};
四、翻转字符串里的单词(中等)需要加强
解题思路:
先删除多余空格,然后进行多次翻转( 注意erase()时间复杂度为O(n) )
双指针实现冗余空格消除
// 时间复杂度O(n)
class Solution {
public:
void reverse(string& s, int start, int end){ //翻转,区间写法:左闭右闭 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html
for (int i = 0; i < s.size(); ++i) { //
if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。
while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
s[slow++] = s[i++];
}
}
}
s.resize(slow); //slow的大小即为去除多余空格后的大小。
}
string reverseWords(string s) {
removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s, 0, s.size() - 1);
int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。
for (int i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。
start = i + 1; //更新下一个单词的开始下标start
}
}
return s;
}
};
五、剑指offer58Ⅱ左旋转字符串(简单)
解题思路:
第一思路:首先将字符串进行整体反转,然后根据n进行区间反转,即进行三次翻转
1. 暴力解法
// 时间复杂度O(n)
class Solution {
public:
string reverseLeftWords(string s, int n) {
// 暴力解法三次翻转
for (int i=0, j=s.size()-1; i<j; i++, j--) {
swap(s[i], s[j]);
}
for (int i=0, j=s.size()-1-n; i<j; i++, j--) {
swap(s[i], s[j]);
}
for (int i=s.size()-n, j=s.size()-1; i<j; i++, j--) {
swap(s[i], s[j]);
}
return s;
}
};
六、实现strStr()—28找出字符串中第一个匹配项的下标(中等)
解题思路:
第一思路:类似于滑动窗口
1. KMP
KMP 主要应用在字符串匹配上。
KMP 的主要思想是当字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配。什么是前缀表:
next数组就是一个前缀表(prefix table)
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
(举例:要在文本串:abbabaafa中查找是否出现过一个模式串abaaf。)什么是前缀表:记录模式串下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
最长公共前后缀?
文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
正确理解什么是前缀什么是后缀很重要!
那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢?
我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 更准确一些。
因为前缀表要求的就是相同前后缀的长度。
(前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串)
所以字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。 等等…。如何计算前缀表
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。
所以要看前一位的 前缀表的数值。
前一个字符的前缀表的数值是2,所以把下标移动到下标2的位置继续比配,最后就在文本串中找到了和模式串匹配的子串。前缀表与next数组
很多KMP算法都是使用next数组来做回退操作,那么next数组与前缀表什么关系呢?
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。使用next数组来匹配
以下我们以前缀表统一减一之后的next数组来做演示。
有了next数组,就可以根据next数组来匹配文本串s,和模式串t了。
注意next数组是新前缀表(旧前缀表统一减一了)。
构造next数组
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
- 初始化;
- 处理前后缀不相同的情况;
- 处理前后缀相同的情况。
void getNext(int* next, const string& s){ int j = -1; next[0] = j; for(int i = 1; i < s.size(); i++) { // 注意i从1开始 while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 j = next[j]; // 向前回退 } if (s[i] == s[j + 1]) { // 找到相同的前后缀 j++; } next[i] = j; // 将j(前缀的长度)赋给next[i] } }
使用next数组来做匹配
1.1 前缀表统一减一
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
1.2 前缀表(不减一)
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
// 回退是一个过程所以用 while()
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
七、重复的子字符串(简单)
解题思路:
1. 移动匹配
如果 s 内部由重复的子串组成,那么 s+s 中除去首尾还存在 s
// 时间复杂度:O(n)
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
// std::string::npos为静态常量,表示没有找到的情况
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
2. KMP(没有很懂)
// 时间复杂度 O(n)
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++){
while(j >= 0 && s[i] != s[j + 1]) {
j = next[j];
}
if(s[i] == s[j + 1]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
return true;
}
return false;
}
};
字符串部分总结:
- 双指针法
- 翻转字符串
- KMP
Ⅴ、栈和队列部分
栈和队列理论基础
关于栈的四个问题:
- C++中stack是容器吗?
栈和队列是STL(C++标准库)里面的两个数据结构
- 我们使用的stack是属于哪个版本的STL?
- HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。 - P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。 - SGI STL由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。 所以介绍的栈和队列也是SGI STL里面的数据结构。
- 我们使用的STL中stack是如何实现的?
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可拔插的(即我们可以控制使用哪种容器来实现栈的功能)
STL中栈往往不被归类于容器,而被归类为container adapter(容器适配器)。
栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。
使用vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int>> third; // 使用vector为底层容器的栈
指定list为底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
```

- stack提供迭代器来遍历stack空间吗?
栈提供 push 和 pop 等接口,所有元素必须符合后进先出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。不像是set或者map提供迭代器来遍历所有元素。
一、232用栈实现队列(简单)
解题思路:
本题考查的是对栈和队列的理解。
1. 两个栈(入栈和出栈)
class MyQueue {
public:
// 定义两个栈
stack<int> stIn;
stack<int> stOut;
MyQueue() {
}
void push(int x) {
stIn.push(x);
}
int pop() {
// 只有当出栈为空的时候才从入栈导入数据
if (stOut.empty()) {
// 从入栈导出
while (!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
int peek() {
// 只有当出栈为空的时候才从入栈导入数据
if (stOut.empty()) {
// 从入栈导出
while (!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
return result;
}
bool empty() {
if (stOut.empty() && stIn.empty()) {
return true;
} else {
return false;
}
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
2. 推荐使用的代码风格(注重复用)
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stIn.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
// 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
if (stOut.empty()) {
// 从stIn导入数据直到stIn为空
while(!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
/** Get the front element. */
int peek() {
int res = this->pop(); // 直接使用已有的pop函数
stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
return res;
}
/** Returns whether the queue is empty. */
bool empty() {
return stIn.empty() && stOut.empty();
}
};
二、225用队列实现栈(简单)
解题思路:
第一想法:类似于使用两个栈实现队列的做法,但是顺序并未发生改变,所以无效。
1. 两个队列(一个队列作为备用)
class MyStack {
public:
queue<int> que1;
queue<int> que2; // 辅助队列,用来备份
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que1.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que1.size();
size--;
while (size--) { // 将que1 导入que2,但要留下最后一个元素
que2.push(que1.front());
que1.pop();
}
int result = que1.front(); // 留下的最后一个元素就是要返回的值
que1.pop();
que1 = que2; // 再将que2赋值给que1
while (!que2.empty()) { // 清空que2
que2.pop();
}
return result;
}
/** Get the top element. */
int top() {
// 1. 使用back()
// return que1.back();
// 2. 使用pop(),然后push(),保证元素不发生变化
int result = this->pop();
qu1.push(result);
return result;
}
/** Returns whether the stack is empty. */
bool empty() {
return que1.empty();
}
};
三、20有效的括号(简单)
解题思路:
第一想法:通过滑动窗口一样进行遍历,但是题目表述感觉有点问题
1. 栈(括号匹配是使用栈解决的经典问题)
// 时间复杂度 O(n)
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求
stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况:遍历字符串匹配的过程中栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
return st.empty();
}
};
四、1047删除字符串中的所有相邻重复项(简单)
解题思路:
同上一题,遍历字符并入栈,最后一个读取栈的操作 注意:当栈为空时,top()可能会报错!!!
1. 栈
// 时间复杂度 O(n)
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
for (int i=0; i<s.size(); i++) {
// 如果匹配
if (st.empty() || st.top() != s[i]) {
st.push(s[i]);
}
else {
st.pop();
}
}
string result = "";
while (!st.empty()) {
result += st.top();
st.pop();
}
reverse(result.begin(), result.end()); // 此时字符串需要反转一下
return result;
}
};
2. 字符串直接当栈使用
// 时间复杂度 O(n)
class Solution {
public:
string removeDuplicates(string S) {
string result;
for(char s : S) {
if(result.empty() || result.back() != s) {
result.push_back(s);
}
else {
result.pop_back();
}
}
return result;
}
};
五、150逆波兰表达式求值(中等)
解题思路:
第一想法:将字符串进行入栈操作,如果遇到运算符,则取出最近的两个元素并进行运算,再将结果入栈
1. 栈
class Solution {
public:
int evalRPN(vector<string>& tokens) {
// 力扣修改了后台测试数据,需要用longlong
stack<long long> st;
for (int i = 0; i < tokens.size(); i++) {
if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
long long num1 = st.top();
st.pop();
long long num2 = st.top();
st.pop();
if (tokens[i] == "+") st.push(num2 + num1);
if (tokens[i] == "-") st.push(num2 - num1);
if (tokens[i] == "*") st.push(num2 * num1);
if (tokens[i] == "/") st.push(num2 / num1);
} else {
st.push(stoll(tokens[i]));
}
}
int result = st.top();
st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事)
return result;
}
};
2. 我的做法
class Solution {
public:
int typeCal(int a, int b, string c) {
if (c == "+") {return a+b;}
else if (c == "-") {return a-b;}
else if (c == "/" && b != 0) {return a/b;}
else {return a*b;}
}
int typeConv(string s) {
int num = 0;
std::istringstream ss(s);
ss >> num;
return num;
}
int evalRPN(vector<string>& tokens) {
stack<string> st;
for (int i = 0; i<tokens.size(); i++) {
if (st.empty() || (tokens[i] != "+" && tokens[i] != "-" && tokens[i] != "*" && tokens[i] != "/")) {
st.push(tokens[i]);
}
else {
// 取出栈,进行运算,存入栈
string a = st.top();
st.pop();
string b = st.top();
st.pop();
st.push(std::to_string(typeCal(typeConv(b), typeConv(a), tokens[i])));
}
}
return typeConv(st.top());
}
};
六、滑动窗口最大值(困难)
解题思路
1. 暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,时间复杂度O(n×k) (超时);2. 使用队列
单调队列
// 时间复杂度O(n)
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
que.push(nums[i]);
}
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
紧急知识补充
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
数据结构 (优先级队列)- - - 堆排序:
- 大顶堆:最大元素在最上面
- 小顶堆:最大元素在最下面
七、347前K个高频元素(中等)
解题思路:
第一想法:先将数组进行排序,然后对重复数进行统计
小顶堆
class Solution {
public:
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map<nums[i],对应出现的次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
扩展与总结
- 在工业级别代码开发中,最忌讳的就是实现一个类似的函数,直接把代码站过来改一下就完事,一定要懂得复用,很容易出问题。
- 在企业项目开发中,尽量不要使用递归!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
- 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
- 栈里面的元素在内存中是连续分布的吗?
陷进1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不连续分布
的
陷阱2:缺省情况下,默认底部容器是deque,而deque在内存中的数据分布是不连续的。
Ⅵ、二叉树
二叉树理论基础
1. 二叉树的种类
- 满二叉树:一棵二叉树只有度为0的节点和度为2的结点,并且度为0的节点在同一层上,则这颗二叉树为满二叉树(深度为k,有2^k -1 个节点 的二叉树)
- 完全二叉树:除了最底层节点没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
- 二叉搜索树:二叉搜索树是一个有序树。1) 若左子树不空,则左子树上所有结点的值均小于它的根节点的值;2)若右子树不空,则右子树上所有节点的值均大于它的根节点的值;3) 左、右子树也分别为二叉排序树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。
2. 二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。链式存储方式就用指针,顺序存储的方式就是用数组。
顺序存储的元素在内存是连续分布的,链式存储则是通过指针把分布在各个地址的节点串联一起。
3. 二叉树的遍历方式
- 深度优先遍历:先往深走,遇到叶子节点再往回走;
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后续遍历(递归法,迭代法)
前中后指的是中间节点的遍历顺序。
- 广度优先遍历:一层一层的去遍历。
- 层次遍历(迭代法)
4. 二叉树的定义:
二叉树有两种存储方式 顺序存储 和 链式存储,顺序存储就是用数组来存。
链式存储定义:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x): val(x), left(NULL), right(NULL) {}
};
二叉树的递归遍历
递归三要素:
- 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型;
- 确定终止条件:写完递归算法运行时,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出;
- 确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
1. 144二叉树的前序遍历(简单)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode *cur, vector<int> &result) {
if (cur == NULL) return;
result.push_back(cur->val);
traversal(cur->left, result);
traversal(cur->right, result);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
// 前序遍历
traversal(root, result);
return result;
}
};
2. 145二叉树的后序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode* cur, vector<int> &vec) {
if (cur == nullptr) return;
// 后序遍历---左右中
traversal(cur->left, vec);
traversal(cur->right, vec);
vec.push_back(cur->val);
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
3. 94二叉树的中序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode* cur, vector<int> &vec) {
if (cur == nullptr) return;
// 中序遍历---左中右
traversal(cur->left, vec);
vec.push_back(cur->val);
traversal(cur->right, vec);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
二叉树的迭代遍历
1. 前序遍历(迭代法)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return result;
}
};
2. 中序遍历(迭代法)
class Solution{
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode *cur = root;
while (cur != NULL || !st.empty()) {
// 访问到最底层
if (cur != NULL) {
st.push(cur);
cur = cur->left; // 左
}
else {
// 找到最底层,栈弹出
cur = st.top();
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
3. 后续遍历(迭代法)
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if (node->right) st.push(node->right); // 空节点不入栈
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
二叉树的统一迭代法
1. 中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root!=NULL) st.push(root);
while (!st.empty()) {
TreeNode* cur = st.top();
if (cur!=NULL) {
st.pop();
if (cur->right) st.push(node->right); // 右
st.push(cur); // 中
st.push(NULL);
if (cur->left) st.push(cur->left); // 左
} else {
st.pop();
cur = st.top();
st.pop();
result.push_back(cur->val);
}
}
return result;
}
};
2. 前序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root!=NULL) st.push(root);
while (!st.empty()) {
TreeNode* cur = st.top();
if (cur!=NULL) {
st.pop();
if (cur->right) st.push(node->right); // 右
if (cur->left) st.push(cur->left); // 左
st.push(cur); // 中
st.push(NULL);
} else {
st.pop();
cur = st.top();
st.pop();
result.push_back(cur->val);
}
}
return result;
}
};
3.后序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root!=NULL) st.push(root);
while (!st.empty()) {
TreeNode* cur = st.top();
if (cur!=NULL) {
st.pop();
st.push(cur); // 中
st.push(NULL);
if (cur->right) st.push(node->right); // 右
if (cur->left) st.push(cur->left); // 左
} else {
st.pop();
cur = st.top();
st.pop();
result.push_back(cur->val);
}
}
return result;
}
};
102二叉树的层序遍历(中等)
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
for (int i=0; i<size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
一、翻转二叉树(简单)
解题思路:
第一思路:通过层序遍历后翻转
1. 层序遍历(递归)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) return root;
TreeNode* tmp = root->right;
root->right = root->left;
root->left = tmp;
invertTree(root->right);
invertTree(root->left);
return root;
}
};
2. 前序遍历(迭代法)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) return root;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
swap(node->right, node->left);
if(node->right) st.push(node->right); // 右
if(node->left) st.push(node->left); // 左
}
return root;
}
};
二、二叉树中期回顾:589N叉树的前序遍历、590N叉树的后序遍历 (简单)
解题思路
第一想法:使用递归
1. 递归
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
void helper(const Node* root, vector<int> & res) {
if (root == nullptr) {
return;
}
res.emplace_back(root->val);
for (auto & ch : root->children) {
helper(ch, res);
}
}
vector<int> preorder(Node* root) {
vector<int> res;
helper(root, res);
return res;
}
};
三、对称二叉树(简单)
解题思路:
第一思路:前序遍历的结果与翻转之后的结果长度进行对比,如果不相等返回false,题目都没有看清楚!!!
1. 递归(自己的蠢办法)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void helper(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) {
vec.push_back(111);
return;
}
vec.push_back(cur->val);
helper(cur->left, vec);
helper(cur->right, vec);
}
void helper2(TreeNode* cur, vector<int>& vec2) {
if (cur == nullptr) {
vec2.push_back(111);
return;
}
vec2.push_back(cur->val);
swap(cur->left, cur->right);
helper2(cur->left, vec2);
helper2(cur->right, vec2);
}
bool isSymmetric(TreeNode* root) {
// 前序遍历
vector<int> vec;
helper(root, vec);
// 翻转
vector<int> vec2;
helper2(root, vec2);
for (int i=0; i<vec.size(); i++) {
if (vec[i] != vec2[i]) {
cout << vec[i] << " " << vec2[i] << endl;
return false;
}
}
return true;
}
};
2. 递归
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 递归
// 1.确定参数和返回值
bool compare(TreeNode* left, TreeNode* right) {
// 2. 退出条件
if (left==nullptr && right !=nullptr) return false;
else if (right == nullptr && left != nullptr) return false;
else if (right == nullptr && left == nullptr) return true;
else if (left->val != right->val) return false;
// 3. 单层逻辑
bool outside = compare(left->left, right->right);
bool inside = compare(left->right, right->left);
return outside && inside;
}
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return compare(root->left, root->right);
}
};
3. 迭代(队列)
跟递归的逻辑相似
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
queue<TreeNode*> que;
que.push(root->left); // 将左子树头结点加入队列
que.push(root->right); // 将右子树头结点加入队列
while (!que.empty()) { // 接下来就要判断这两个树是否相互翻转
TreeNode* leftNode = que.front(); que.pop();
TreeNode* rightNode = que.front(); que.pop();
if (!leftNode && !rightNode) { // 左节点为空、右节点为空,此时说明是对称的
continue;
}
// 左右一个节点不为空,或者都不为空但数值不相同,返回false
if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) {
return false;
}
que.push(leftNode->left); // 加入左节点左孩子
que.push(rightNode->right); // 加入右节点右孩子
que.push(leftNode->right); // 加入左节点右孩子
que.push(rightNode->left); // 加入右节点左孩子
}
return true;
}
};
四、二叉树的最大深度(简单)
解题思路:
第一思路:递归 + 栈,但是感觉不对
1. 后序遍历(递归)高度
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 1. 确定返回值及参数
int getDepth(TreeNode* node) {
// 2. 条件
if (node == nullptr) return 0;
// 3. 单层逻辑
int leftDepth = getDepth(node->left);
int rightDepth = getDepth(node->right);
int result = 1 + max(leftDepth, rightDepth);
return result;
}
int maxDepth(TreeNode* root) {
return getDepth(root);
}
};
2. 前序遍历(递归、回溯)深度
class solution {
public:
int result;
void getdepth(TreeNode* node, int depth) {
result = depth > result ? depth : result; // 中
if (node->left == NULL && node->right == NULL) return ;
if (node->left) { // 左
depth++; // 深度+1
getdepth(node->left, depth);
depth--; // 回溯,深度-1
}
if (node->right) { // 右
depth++; // 深度+1
getdepth(node->right, depth);
depth--; // 回溯,深度-1
}
return ;
}
int maxDepth(TreeNode* root) {
result = 0;
if (root == NULL) return result;
getdepth(root, 1);
return result;
}
};
//简化版本
class solution {
public:
int result;
void getdepth(TreeNode* node, int depth) {
result = depth > result ? depth : result; // 中
if (node->left == NULL && node->right == NULL) return ;
if (node->left) { // 左
getdepth(node->left, depth + 1);
}
if (node->right) { // 右
getdepth(node->right, depth + 1);
}
return ;
}
int maxDepth(TreeNode* root) {
result = 0;
if (root == 0) return result;
getdepth(root, 1);
return result;
}
};
五、n叉树的最大深度(简单)
解题思路:
第一思路:层序遍历会更加简单
1. 层序遍历
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
int maxDepth(Node* root) {
queue<Node*> que;
if (root != NULL) que.push(root);
int result = 0;
while(!que.empty()) {
int size = que.size();
result ++;
for (int i=0; i<size; i++) {
Node* node = que.front();
que.pop();
for (int j = 0; j < node->children.size(); j++) {
if (node->children[j]) que.push(node->children[j]);
}
}
}
return result;
}
};
2. 递归法
class solution {
public:
int maxDepth(Node* root) {
if (root == 0) return 0;
int depth = 0;
for (int i = 0; i < root->children.size(); i++) {
depth = max (depth, maxDepth(root->children[i]));
}
return depth + 1;
}
};
六、二叉树的最小深度(简单)
解题思路:
1. 递归法
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right != NULL) {
return 1 + minDepth(root->right);
}
if (root->left != NULL && root->right == NULL) {
return 1 + minDepth(root->left);
}
return 1 + min(minDepth(root->left), minDepth(root->right));
}
};
2. 层序遍历(迭代)
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == NULL) return 0;
int depth = 0;
queue<TreeNode*> que;
que.push(root);
while(!que.empty()) {
int size = que.size();
depth++; // 记录最小深度
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
if (!node->left && !node->right) { // 当左右孩子都为空的时候,说明是最低点的一层了,退出
return depth;
}
}
}
return depth;
}
};
七、完全二叉树的节点个数(中等)
解题思路:
想法:层序遍历,2^层数-1+个数
1. 层序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 层序遍历-迭代
int countNodes(TreeNode* root) {
queue<TreeNode*> que;
if(root != nullptr) que.push(root);
int f = 0;
while (!que.empty()) {
int num = 0;
int size = que.size();
f ++;
for (int i=0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
// 左节点
if (node->left) {
num ++;
que.push(node->left);
}
// 右节点
if (node->right) {
num ++;
que.push(node->right);
} else {
return pow(2, f) - 1 + num;
}
}
}
return 0;
}
};
八、平衡二叉树(简单)
解题思路:
1. 递归
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 1. 确定返回值及参数
int getHeight(TreeNode* node) {
// 2. 退出条件
if (node == NULL) {
return 0;
}
int leftHeight = getHeight(node->left);
if (leftHeight == -1) return -1;
int rightHeight = getHeight(node->right);
if (rightHeight == -1) return -1;
return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);
}
bool isBalanced(TreeNode* root) {
return getHeight(root) == -1 ? false : true;
}
};
九、二叉树的所有路径(简单)
解题思路:
使用到了回溯的思想,将子节点遍历之后,再移除
1. 递归(+回溯的思想)
class Solution {
public:
// 确定返回值及参数列表
void treePaths(TreeNode* node, vector<int> &path, vector<string> &result) {
// 中节点
path.push_back(node->val);
// 确定叶子节点
if (node->left==nullptr && node->right==nullptr) {
string sPath;
for (int i=0; i<path.size() - 1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
sPath += to_string(path[(path.size() - 1)]);
result.push_back(sPath);
return;
}
// 单层逻辑
if (node->left) {
treePaths(node->left, path, result);
path.pop_back(); // 回溯
}
if (node -> right) {
treePaths(node->right, path, result);
path.pop_back(); //回溯
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<int> path;
vector<string> result;
if (root == nullptr) return result;
treePaths(root, path, result);
return result;
}
};
十、左叶子之和
解题思路:
递归,将满足条件的节点的值加入容器中
1.递归
class Solution {
public:
void leftLeaves(TreeNode* node, vector<int> &vec) {
// 判断是否存在左节点
if (node == nullptr ) return;
// 判断是否为叶子节点
if (node->left != nullptr && node->left->left == nullptr && node->left->right == nullptr) {
vec.push_back(node->left->val);
}
// 节点非空
if (node->right) leftLeaves(node->right, vec);
if (node->left) leftLeaves(node->left, vec);
}
int sumOfLeftLeaves(TreeNode* root) {
int sum = 0;
vector<int> vec;
if (root == nullptr) return sum;
leftLeaves(root, vec);
for (int i=0; i<vec.size(); i++) {
sum += vec[i];
}
return sum;
}
};
十一、513找树左下角的值(中等)
解题思路:
层序遍历(广度优先搜索),然后最后一层的第一个就是取值
1. 层序遍历
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> res;
if (root != nullptr) que.push(root);
while (!que.empty()) {
vector<int> vec;
int size = que.size();
for (int i = 0; i<size; i++) {
TreeNode* cur = que.front();
que.pop();
vec.push_back(cur->val);
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
res.push_back(vec);
}
// 取最后一层的第一个元素
return res[(res.size()-1)][0];
}
};
2. 递归+回溯
十二、112路径总和(简单)
解题思路:
递归+回溯,跟前面的所有路径一样的思路
1. 递归+回溯(自己通过相加比较比较繁琐)
class Solution {
public:
bool treePath(TreeNode* root, vector<int> &path, int targetSum, bool &res) {
// 添加中间节点
path.push_back(root->val);
// 到达叶子节点,进行路径求和, 并退出循环
if (root->left == nullptr && root->right == nullptr) {
int result = 0;
for (int i = 0; i < path.size(); i++) {
result += path[i];
}
if (result == targetSum) res = true;
}
// 单层逻辑
if (root->left) {
treePath(root->left, path, targetSum, res);
path.pop_back(); // 回溯
}
if (root->right) {
treePath(root->right, path, targetSum, res);
path.pop_back(); //回溯
}
return false;
}
bool hasPathSum(TreeNode* root, int targetSum) {
vector<int> path;
bool res = false;
if (root == nullptr) return false;
treePath(root, path, targetSum, res);
return res;
}
};
十三、113路径总和II(中等)
解题思路:
递归+回溯,然后判断
1.递归+回溯
class Solution {
public:
void treePath(TreeNode* node, vector<vector<int>> &result, vector<int> &path, int targetSum) {
// 添加中间节点
path.push_back(node->val);
if (node->left == nullptr && node->right == nullptr) {
int sum = 0;
for (int i = 0; i < path.size(); i++) {
sum += path[i];
}
if (sum == targetSum) result.push_back(path);
}
if (node->left) {
treePath(node->left, result, path, targetSum);
path.pop_back();
}
if (node->right) {
treePath(node->right, result, path, targetSum);
path.pop_back();
}
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> result;
vector<int> path;
if (root == nullptr) return result;
treePath(root, result, path, targetSum);
return result;
}
};
十四、106从中序与后续遍历序列构造二叉树(中等)
解题思路:
根据后续遍历,依次进行前后读取。
存在问题
1.递归+分割数组
class Solution {
public:
TreeNode* traversal(vector<int>&inorder, vector<int>& postorder) {
if (postorder.size() == 0) return nullptr;
int nodeValue = postorder[(postorder.size()-1)];
TreeNode* root = new TreeNode(nodeValue);
postorder.pop_back();
// 切割点
int delimiterIndex;
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if (inorder[delimiterIndex] == nodeValue) break;
}
// 切割中序遍历,分成左右两边
vector<int> leftInorder(inorder.begin(), inorder.begin()+delimiterIndex);
vector<int> rightInorder(inorder.begin()+delimiterIndex+1, inorder.end());
// 切割后序遍历
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
return traversal(inorder, postorder);
}
};
Ⅶ、回溯算法
回溯算法理论基础
回溯法也叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯
动态规划
动态规划五部曲
- 确定dp数组和下标含义;
- 确定递推公式;
- dp数组如何初始化;
- 确定遍历顺序;
- 举例推导dp数组。
一、简单斐波那契数(509)
题解:
class Solution {
public:
int fib(int n) {
if (n <= 1) return n;
// 1. 确定dp数组以及下标的含义
vector<int> dp(n + 1);
// 2. 确定递推公式,题目已给出F(n) = F(n - 1) + F(n - 2)
// 3. dp数组初始化
dp[0] = 0;
dp[1] = 1;
// 4. 确定遍历顺序
for (int i = 2; i <= n; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
// 5.举例推导dp数组
}
};