似乎以前都只注重于把这道题解出来,而没有在意做题时的“心路历程”,也没有归类总结,更没有去回顾····
这样的刷题,如同空中楼阁一样,轻飘飘的,长远来看没有任何益处。
因此在平时练习题的时候,最重要的先写清楚思路,再进行代码的编写。
排序算法
注:外部排序其实大数据量下的排序,也是基于内部排序进行的。
注:下面是各类排序的一个简单实现,以升序排列为例,原址为链接。
冒泡排序
每一趟通过交换,将遍历到的最大值传递到末尾,每一趟完成之后修改末尾下标。
//冒泡排序
void maoPaoSort(int array[]){
for (int i = 0; i < N-1; i++){
//逐渐收缩比较的范围,将大的往上退
for (int j = 0; j < N-1-i; j++){
if (array[j] > array[j+1]){
int temp = array[j+1];
array[j+1] = array[j];
array[j] = temp;
}
}
printf("this is %d:", i + 1);
printArray(array);
}
}
选择排序
每一趟从当前选择值中选择一个最小值,将最小值与前面第N个(已排序)数字互换。
void choiceSort(int array[]){
for (int i = 0; i < N; i++){
int k = i;
for (int j = k + 1; j < N; j++){
if (array[j] < array[k]){
k = j;
}
}
//找到了最小的值再交换
if(k!=i)//说明根本不用交换
{
int temp = array[i];
array[i] = array[k];
array[k] = temp;
}
}
}
插入排序
类似于扑克牌洗牌,从后面选择一个数,插入到已排序的合适位置。
该算法有个问题,那就是需要不停的往后挪动数组,很重要!
void insertSort(int array[]){
//从第二个元素开始,加入第一个元素是已排序数组
for (int i = 1; i < N; i++){
//待插入元素 array[i]
if (array[i] < array[i - 1]){
int wait = array[i];
int j = i;
//开始挪动数组
while (j > 0 && array[j - 1] > wait){
//从后往前遍历已排序数组,若待插入元素小于遍历的元素,则遍历元素向后挪位置
array[j] = array[j - 1];
j--;
}
array[j] = wait;
}
}
}
希尔排序
https://blog.csdn.net/weixin_44915226/article/details/119510328
其实是插入排序的分组版本。
# 插入排序
def insert_sort(alist):
n = len(alist)
for i in range(1,n):
while (i>0):
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
return alist
# 希尔排序
def shell_sort(alist):
n = len(alist)
gap = n // 2
while gap > 0:
for i in range(gap,n):
j = i
while j > 0:
if alist[j] > alist[j-gap]:
alist[j], alist[j-gap] = alist[j-gap], alist[j]
j -= gap
else:
break
gap //= 2
return alist
二分查找
相关题型:
35.搜索插入位置,这道题是在寻找插入的位置,最后返回left即可,因为跳出while循环,left脚下就是答案!
二分查找的常用框架如下:
查一个数
查左边界
之所以要以left为最终的判断,是因为left可能恰好越过界,就踩在那个点上!
查右边界
之所以要以right为最终的判断,是因为right可能恰好越过界,就踩在那个点上!
注:不要出现else,而是用if else 这样看代码时能很快知道到底是什么逻辑
解题思路:如何才能够找到合适的吃香蕉速度?
- 速度最小可能为多少? 1根/小时
- 速度最大可能为多少? 因为每次只能吃一堆,即便还有胃口也不会再去吃了,所以最大的速度是max(piles)/根
- 因此,就是从min~max之中进行遍历,找到最小的那个速度
解题思路:
- 想一想,最小的速度可以为多少?max(weights);
- 最大的速度可以为多少?sum(weights);
- 所以,这又是一个二分查找求左边界的问题!链接
解题思路:
第一钟方法:将数组展开为一维数组,再排序,再获取第k小的元素;
第二种方法:使用堆排序,注意遍历的顺序和堆的大小,如果是大顶堆,那么需要从矩阵右下开始遍历;
第三种方法:利用矩阵的特性以及二分的思想。
首先,初始最小和初始最大分别为左上与左下,可用mid作为中间值。
根据升序矩阵的性质,不大于它的元素个数其实就是下图路径左边的个数。
如何求左边的个数?从左下角起步,如何当前值小于等于mid,那么就往右边走,并把当前列的个数**(因为列首小于列尾啊)**记入总个数,否则往上走。
我一开始有一个疑惑:mid一定会在矩阵中吗?
会的,如果不会,那么就不会出现这个数字,题错了!
如何计算个数?
如何进行边界的更新?
剑指offer中有一道相关题,240. 搜索二维矩阵 II,直接从右上角开始遍历;
快速排序
快速排序其实也是分治递归的思想
具体有两种实现思路:
第一个是左右两个值都找到才交换,最后再与基准值进行交换;
第二个是左右两个找到了就交换,就好像两边在互相挖坑让对方跳;
第三个时
void quickSort(int array[], int left, int right){
if (left > right){
return;
}
int i, j, temp;
i = left;
j = right;
//以最左边的数作为基准数
temp = array[left];
//第一种
while (i != j){
//先从右边开始找小于temp的元素 注意等号
while (array[j] >= temp && i < j) {
j--;
}
//再从左边开始找大于temp的元素
while (array[i] <= temp && i < j){
i++;
}
//左右哨兵均找到满足要求的数后,交换这两个数
if (i < j){
int change = array[i];
array[i] = array[j];
array[j] = change;
}
}
// 出循环表明i==j
// 因此将基准数归位,此时基准数左边的数均小于基准数,右边的数均大于基准数
array[left] = array[j];
array[j] = temp;
//然后递归处理基准左边未排序的数,和基准右边的数
quickSort(array, left, i-1);
quickSort(array, i + 1, right);
}
//第二种
void QuickSort2(int a[],int l ,int h)
{
int po;
int high = h,low = l;
if( l < h )
{
po = a[l];
while( low < high)
{
while( low < high && a[high] >= po ) high--;
a[low] = a[high]; //相互跳坑
while( low < high && a[low] <= po ) low++;
a[high] = a[low];
}
a[low] = po; //把最后一个坑给补上
QuickSort2(a,l,low-1);
QuickSort2(a,low+1,h);
}
}
//第三种
当初看这个方法时有个疑惑,为什么要进行:array[left] = array[j];array[j] = temp;
在跳出循环时,j所指向的数必定小于基准数,两者进行换位可以保证左边的数小于基准数,右边的数大于基准数。
两种方法:
第一种:先将排序再寻找第K大数(冒泡、快速、堆,等到都属于这个方法)
第二种:借助快速排序的特点,通过阶段性快排的下标来判断!降序排列!
class Solution {
public:
int target;
int findKthLargest(vector<int>& nums, int k) {
if(nums.size()<k){
return -1;
}
target = k;
int index = quickSort(nums,0,nums.size()-1);
return nums[index];
}
int quickSort(vector<int>&nums,int left,int right){
int temp = nums[left];
int i=left;
int j = right;
while(i!=j){
//从右往左(我这是降序排列!)
while(i<j&&nums[j]<=temp){
j--;
}
//左往右
while(i<j&&nums[i]>=temp){
i++;
}
if(i!=j){
int tempnum= nums[j];
nums[j]=nums[i];
nums[i]=tempnum;
}
}
//此时 i=j
nums[left]=nums[i];
nums[i]=temp;
if(i==target-1){
return i;
}
else if(i<target-1){
return quickSort(nums,i+1,right);
}
else {
return quickSort(nums,left,i-1);
}
}
};
堆排序的实现
堆排序是一种选择排序,是利用堆这种数据结构而设计的一种排序算法,它的最坏,最好,平均时间复杂度均O(nlogn),它也是不稳定排序。 首先简单了解下堆——堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:大顶堆,主要是用来实现一个升序排列,(⊙﹏⊙)?把大的拿出来不应该降序吗?
因为堆排序一般是基于数组实现,且将出堆的元素放在数组末尾,可以省去数组移动!弹出一个元素之后,再对剩下的进行一次建堆,如此反复;
小顶堆,主要是实现降序,顺序同上; 有关堆排序的具体实现细节可以参考动图:1.7 堆排序-菜鸟教程。 堆排序demo > 因为堆是一棵完全二叉树,所以一般可以用数组来实现。数组的下标对应堆中节点的编号。为方便起见,我们假设数组下标从 1 开始。那么对于堆中每个节点与其左右子节点的编号关系都有: > > + leftID = fatherID 2 > + rightID = fatherID 2 + 1 > + fatherID = sonID / 2 > > 示例如下图:


cpp
class Solution {
enum { MAXN = 10000 };
int n;
int heap[MAXN];
inline int& getRef(int root) {
return heap[root-1];
}
public:
Solution() : n(0) {}
void push(int v) {
heap[n++] = v;
//开始对洗新加的元素进行排序,直接判断其是否与父节点大小
//for(int pos = n, nextPos = pos>>1; pos > 1
// && getRef(pos) > getRef(nextPos); pos = nextPos, nextPos >>= 1) {
// swap(getRef(pos), getRef(nextPos));
//}
//对上面的改版,更容易理解
int pos=n,nextPos=pos>>1;
//如果新加的节点比其父节点大,那就需要交换
while(pos>1>1&&getRef(pos) > getRef(nextPos)){
swap(getRef(pos), getRef(nextPos));
pos = nextPos, nextPos >>= 1
}
}
int pop() {
//交换顶元素到末尾,并将其剪短,表示排序完毕
swap(getRef(1), getRef(n));
int res = heap[--n];
for(int root = 1; ; ) {
//获取左右子节点
int left = root<<1;
int right = root<<1|1;
//看看现在父节点是否大于其子节点,不大于则交换
//注意要查看节点是否越界
if(right <= n && getRef(root) < max(getRef(left), getRef(right))) {
if(getRef(left) > getRef(right)) {
swap(getRef(left), getRef(root));
root = left;
} else {
swap(getRef(right), getRef(root));
root = right;
}
//注意要查看节点是否越界
} else if (left <= n && getRef(root) < getRef(left)) {
swap(getRef(left), getRef(root));
root = left;
break;
} else {
break;
}
}
return res;
}
int size() const {
return n;
}
vector<int> getLeastNumbers(vector<int>& arr, int k) {
for(auto v : arr) {
this->push(v);
if(this->size() > k) {
this->pop();
}
}
return vector<int>(heap, heap+k);
}
};
## 归并排序
> 归并排序,一般是用递归实现, 分为两个阶段, 分组和合并.
>
> 分组即是将原数组不停的切分为两半,直到每一部分仅有1个元素为止,合并即是将两个分组合并,由于此时两个分组都处于已排序状态,可以很快的排序.
>
> 一般来说,n个数据大致会分为logN层,每层执行merge的总复杂度为O(n), 所以总的复杂度为O(nlogn)。
>
> 需要临时占用部分内存空间.
>
cpp
#include <iostream>
using namespace std;
int arrs[] = { 23, 65, 12, 3, 8, 76, 345, 90, 21, 75, 34, 61 };
int arrLen = sizeof(arrs) / sizeof(arrs[0]);
int * tempArr = new int[arrLen];
void mergeArray(int * arrs, int * tempArr, int left, int middle, int right){
int i = left, j = middle ;
int m = middle + 1, n = right;
int k = 0;
//直接在新数组里,依次将两个数组合并进去!
// 用空间换时间!
while(i <= j && m <= n){
if(arrs[i] <= arrs[m])
tempArr[k++] = arrs[i++];
else
tempArr[k++] = arrs[m++];
}
//防止还有数据没有完全被传递干净
while(i <= j)
tempArr[k++] = arrs[i++];
while(m <= n)
tempArr[k++] = arrs[m++];
//再把合并的数组拷贝回去,不然递归回去之后数组根本没有改变
for(i=0; i < k; i++)
arrs[left + i] = tempArr[i];
}
void mergeSort(int * arrs, int * tempArr, int left, int right){
if(left < right){
int middle = (left + right)/2;
mergeSort(arrs, tempArr, left, middle);
mergeSort(arrs, tempArr, middle + 1, right);
mergeArray(arrs, tempArr, left, middle, right);
}
}
int main()
{
mergeSort(arrs, tempArr, 0, arrLen-1);
for (int i = 0; i < arrLen; i++)
cout << arrs[i] << endl;
return 0;
}
148.排序链表
> 

[剑指offer51:数组中的逆序对](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)
> 
>
> 归并排序;
>
> **解题思路**:
>
> + 将问题分解为两部分来求解
> + 这样求逆序对的问题,就转换成为了在归并排序的过程中**做一些额外工作:计算逆序对的问题。**
> + 先求中点,而后递归调用分割函数,将原数组切分(用下标来表示切分)
> + 当数组分割成最小单元(1个数)时,开始进行合并
> + 合并前先复制一遍原数组,而后将顺序的数字重新放入原数组中。在合并时,根据前后的大小关系,计算逆序对;
> + 返回最终的逆序结果
> + 这个题解比较容易理解:[链接](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/jian-zhi-offer-51-shu-zu-zhong-de-ni-xu-pvn2h/869354)
>
> **二刷心得:**
>
> + 终于什么参考都没看,用归并排序将其解出来了,有一些细节要注意
> + 一是,复制数组时,只复制[left,right]之中的数据,否则会超时;(**或者直接复制整个数组,这样就不会有坐标转换的步骤,并且速度更快!**)省去了每次都构造vector的时间开销!
> + 二是,注意坐标的转换,以及边界条件等问题,最好在本子上画出来推导一下;
>
[23.合并K个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/)
> 
>
> 解题思路:
>
> + 利用优先队列,实现堆排序,而后依次取出最小的节点即可,注意重载运算符或仿函数的方法;
> + 归并排序,**分割成两两合并链表的问题**,最后依次递归:[链接](https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/shun-xu-he-bing-si-lu-jian-dan-by-climit-qxo9/)
>
cpp
//优先队列,堆排序
class Solution {
public:
struct cmp{ //对新的数据类型的<进行重写
bool operator()(ListNode a,ListNode b){
return a->val > b->val;
}
};
ListNode mergeKLists(vector<ListNode>& lists) {
priority_queue## C++如何使用优先队列?
priority_queue<Type, Container, Functional>
传入基本数据类型时,**默认是大顶堆.**
priority_queue <int,vector<int>,greater<int> > q;** (小顶堆)——降序**
priority_queue <int,vector<int>,less<int> >q**;(大顶堆)——升序**
**重载运算符或重写仿函数:**
cpp
//方法1
struct cmp //运算符重载<
{
int x;
tmp1(int a) {x = a;}
bool operator<(const tmp1& a) const
{
return x < a.x; //大顶堆
}
};
//方法2(常用!)
struct cmp //重写仿函数
{
bool operator() (tmp1 a, tmp1 b)
{
return a.x < b.x; //大顶堆 返回true表示优先级高!
}
};
```
# 树的王国
> 先看看这篇文章(从普通树到二叉平衡树、B-Tree等):链接
>
## 二叉查找/搜索树(BST,Binary Search Tree)
特点:二分的思想
1、若它的左子树不为空,则*左子树上所有的节点值都小于它的根节点值;
2、若它的右子树不为空,则右子树上所有的节点值均大于它的根节点值;
3、它的左右子树也分别可以充当为二叉查找树;
4、没有相等的节点。(此点存疑,根据实际应用来调整,或是在节点增加一个count计数,但代价大);
实现思路:
访问:找一个值,只需要从根节点出发,与根节点去比较,若大于根节点去右子树找,若小于去左子树中找(每次淘汰一半的子树),这样只需要 O(logn)就能找到你要找的元素;
插入:先利用访问找到合适的位置,而后将需要插入的数字根据大小放置在左/右边;
如下,替换掉12原本的位置,12作为13的左子节点:
删除:删除情况比较复杂,这需要根据左右两子树的情况来确定最终的值,例如,如果 A 有两个子节点, 那么我们就以右子树的最小节点取代 A,(从右子节点开始,一直走到左到底就是其右子树的最小节点)将 A 赋予其值,并将其删除:
为什么要这样?因为只有取右子树的最小值,才可以最小程度的影响原树结构;
实现代码:
//https://www.cnblogs.com/WindSun/p/10895787.html
#include <iostream>
using namespace std;
template<typename T>
//BST结点定义
struct BSTNode
{
T data;
BSTNode<T> *lchild, *rchild;//左右子结点
//默认构造函数
BSTNode() :lchild(nullptr), rchild(nullptr) {}
//带参构造函数(初始化列表的方式)
BSTNode(const T d, BSTNode<T>* L = nullptr, BSTNode<T>* R = nullptr) :data(d), lchild(L), rchild(R) {}
};
template<typename T>
class BST
{
public:
BST() :root(nullptr) {}
//构建二叉树
BST(T value) :root(nullptr), InputEnd(value) {
T tempvalue;
cin >> tempvalue;
while (tempvalue!=InputEnd)
{
Insert_(tempvalue, root);
cin >> tempvalue;
}
}
//析构函数
~BST() { Destroy(root); }
//插入
bool Insert(T value) { return Insert_(value, root); }
//插入元素(第二个参数是指针的引用,引用原树的根节点)
bool Insert_(const T& value, BSTNode<T>* &node) {
if (node == nullptr) {
node = new BSTNode<T>(value);
//判断内存是否分配成功
if (node == nullptr)
{
cout << "Memory allocation failed!" << endl;
exit(1);//程序退出
}
return true;
}
else if (value < node->data) {
Insert_(value, node->lchild);
}
else if (value > node->data) {
Insert_(value, node->rchild);
}
else {
return false;//已在树中插入失败(根据情况而定)
}
}
//删除
bool Remove(T value) { return Remove_(value, root); }
//删除一个元素(以node为根的二叉搜索树中删除含x的结点)
bool Remove_(T value, BSTNode<T>* &node) {
BSTNode<T>* temp;
if (node != nullptr) //node不为空进行操作
{
if (value < node->data)
{
Remove_(value, node->lchild);
}
else if (value > node->data)
{
Remove_(value, node->rchild);
}
//找到了要删除的结点
//1.要删除的结点node同时有左右子树
else if (node->lchild != nullptr&&node->rchild != nullptr)
{
temp = node->rchild; //在右子树中搜索中序下的第一个结点
while (temp->lchild != nullptr)
{
temp = temp->lchild;
}
//用右子树中序下的第一个结点的值填充要删除的结点
node->data = temp->data;
//然后再新填充值node的右子树中删除temp的data值
Remove_(node->data, node->rchild);
}
else //不同时有左右子树
{
temp = node; //temp记住要删除的node结点
if (node->lchild == nullptr) //只有右子树
{
node = node->rchild;
}
else //只有左子树
{
node = node->lchild;
}
delete temp; //删除结点
temp = nullptr;
return true;
}
}
else //node为空直接返回false
{
return false;
}
}
//搜索二叉树
bool Search(T value) { return (Search_(value, root) != nullptr) ? true : false; }
//搜索若找到,返回该结点地址,否则返回NULL
BSTNode<T>* Search_(T x, BSTNode<T>* node)
{
if (node == nullptr)
{
return nullptr;
}
else if (x < node->data)
{
return Search_(x, node->lchild);
}
else if (x > node->data)
{
return Search_(x, node->rchild);
}
else
{
return node;
}
}
//中序遍历
void InOrder() { InOrder(root); }
//遍历操作
void InOrder(BSTNode<T>* root)
{
if (root != NULL)
{
InOrder(root->lchild);
cout << root->data << " ";
InOrder(root->rchild);
}
}
//销毁二叉树
void Destroy(BSTNode<T>* &root)
{
if (root == nullptr)
{
return;
}
if (root->lchild != nullptr)
{
Destroy(root->lchild);
}
if (root->rchild != nullptr)
{
Destroy(root->rchild);
}
delete root;
root = nullptr;
}
private:
BSTNode<T> *root;//根节点指针
T InputEnd;//输入结束标志(构建二叉树所用)
};
int main(int argc, char* argv[])
{
//g a e d f h j i l k #
BST<char> tree('#');
tree.InOrder();
cout << endl;
cout << tree.Search('e') << endl;
cout << tree.Insert('z') << endl;
tree.InOrder();
cout << endl;
cout << tree.Remove('z') << endl;
cout << tree.Remove('j') << endl;
tree.InOrder();
cout << endl;
system("pause");
return 0;
}
平衡二叉树/AVL
为了解决二叉搜索树在某些情况下退化成一个有序链表的问题,强制限定左右子树的高度差最多为1;
特点:
BST**的特点+每个节点左右子树的高度差最多等于**1。调整:如何保证调整过后是AVL?不会影响之前的结构吗?
不会,因为在插入过程中是动态维护的,是从叶子节点往上递归进行。 左左型:需要右旋——即:顺时针旋转两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。


各节点的深度和高度如下表:
注意:定义中所谓的“经过”,是指完全略过黑节点,比如002的黑高度为2, 008的黑高度也是2,而不是3,虽然008也是一个黑节点,但是没有完全经过!
调整方法:https://zhuanlan.zhihu.com/p/79980618?utm_source=cn.wiz.note或链接
插入调整:寻找插入点+调整(旋转或上色)
在寻找插入点的时候,算法的思路如下:
- 新插入节点设置为红色(如果插入黑色节点,会导致某路径上的黑色节点数目超过其他路径,违反性质5,相比之下,红色影响较小)
- 新插入节点首先和根节点比较:若根节点不存在即树为空,则新插入节点作为树的根节点插入
- 若根节点存在,则比较两节点大小,确认要插入该节点的左子树或右子树
- 若要插入当前节点左子树,则比较新插入节点和左孩子节点,若左孩子不存在, 则新插入节点作为当前节点的左孩子节点.若左孩子存在,则重复过程2-4
- 若要插入当前节点右子树,则比较新插入节点和右孩子节点, 若右孩子节点不存在,则新插入节点作为当前节点的右孩子节点.若右孩子存在,则重复过程2-4
- 最终新节点作为叶子节点插入
在调整的过程中,主要有以下四种情况:
上面最复杂的其实是case3的case3,采用这种方式之后,可能会造成有两个红色连续,因此就需要以G节点为新节点,继续进行调整,类似于一种递归的过程。详情请参考红黑树的插入。
删除调整:寻找插入点+交换调整
删除大致分为两种情况,一种是删除叶子节点,一种是删除非叶子节点。
如果是删除非叶子节点,一般是采用寻找删除节点的前驱节点或后继节点来交换,从而转换为删除叶子节点。
前驱节点是删除节点的左子树的最大值;
后继节点是删除节点的右子树的最小值;
通过这种互换位置和颜色,删除情形又被分为三种:
- case1:删除的节点为叶子节点
- case2:删除的节点只有左子树
- case3:删除的节点只有右子树
其中case2、case3是一类,因为只有一个子树,那么子树节点必为红色,隐含删除节点必为黑色,根据情况删除即可,参考红黑树删除1.
对于叶子节点情况,就需要进行全局的调整了,直接去看博客红黑树删除2.
简单来说,红黑树和AVL的区别就在于需要考虑到五种规则,但是五种规则在实际使用的时候其实只需要考虑2种:红红不相连、黑的节点相同,调整红黑树的过程就是在考量这两个因素的过程。
红黑树比AVL好在哪?
参考这个答案的说法:链接
Linux内核用红黑树是有官方答案的。
其中第 16 行提到
Red-black trees are similar to AVL trees, but provide faster real-time bounded
worst case performance for insertion and deletion (at most two rotations and
three rotations, respectively, to balance the tree), with slightly slower
(but still O(log n)) lookup time.
红黑树类似于AVL树,但提供更快的实时性,插入和删除的有界最坏情况性能(最多两个分别旋转和三次旋转以平衡树)。查找时间稍慢(但仍为O(log n))。
这个部分来看看二叉树有什么题。缘起(反转、填充、展开为链表)
“快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后续遍历”————labuladong快速排序是寻找分界点,再对两边进行快速排序——类似于前序遍历;归并排序是先对两边进行排序,再到中界进行合并——类似于后序遍历;
写树相关的算法,简单说就是,先搞清楚当前root节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。(这其实就是递归的意义)
思路:
利用递归,反转当前节点的左右节点,而后对左右子树再进行同样的操作;













话说这道题应该咋做?一点头绪都没有。
最后看了答案才明白:利用**前或中或后序遍历**将子树展开成字符串,再利用hash表判断是否具有重复的子树结构。
建议用后续遍历,因为子树是从下往上的这道题深刻地诠释了递归解法的意义,每一个节点只需要知道,以自己为根节点的子树是不是重复的就可以了,那么判断自己是重复的需要哪些条件呢? 需要知道自己的子树结构(局部),以及其他人的子树结构(全局);怎么判断有没有? 用Set或者HashMap即可。 这道题堪称经典!!




详细解释可看:
https://mp.weixin.qq.com/s/ioyqagZLYrvdlZyOMDjrPw
简单来说,跟7.3最后一道题有些类似:需要在二叉树节点中维护额外信息。每个节点需要记录,以自己为根的这棵二叉树有多少个节点。(有了这个信息,我们就可以快速寻找对应大小的节点,且平均时间复杂度为指数级别)



booleanisValidBST(TreeNode root){
return isValidBST(root, null, null);
}
/* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
booleanisValidBST(TreeNode root, TreeNode min, TreeNode max){
// base case
if (root == null) return true;
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if (min != null && root.val <= min.val) return false;
if (max != null && root.val >= max.val) return false;
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValidBST(root.left, min, root) && isValidBST(root.right, root, max);
}
在二叉搜索树中搜索一个数
结合它左大右小的性质:
voidBST(TreeNode root, int target){
if (root.val == target)
// 找到目标,做点什么
if (root.val < target)
BST(root.right, target);
if (root.val > target)
BST(root.left, target);
}
在BST中插入一个数
一旦涉及「改」,函数就要返回TreeNode类型,并且对递归调用的返回值进行接收。
TreeNode insertIntoBST(TreeNode root, int val){
// 找到空位置插入新节点
if (root == null) return new TreeNode(val);
// if (root.val == val)
// BST 中一般不会插入已存在元素
if (root.val < val)
root.right = insertIntoBST(root.right, val);
if (root.val > val)
root.left = insertIntoBST(root.left, val);
return root;
}
在BST中删除一个数
找到删除的那个数不难,难的是删除了之后如何调整改二叉树使其结构正常!! 分为三种情况:- 只有一个节点
- 没有节点
- 有两个节点
https://mp.weixin.qq.com/s/SuGAvV9zOi4viaeyjWhzDw
二叉树序列化
二叉树的序列化与反序列化,无外乎就是一下几种遍历的方式:
前序遍历:这是最容易理解的方式,反序列化提取字符串第一个递归即可;
后序遍历:序列化较简单,难点在于反序列化,需要从后往前,且先构造右子树,再构造左子树。
/******** 先序遍历序列化与反序列化 *************/
class Codec {
public:
string data;
string SEGMENT="|";
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
traverse(root);
data.pop_back(); // 去掉尾部 ,
return data;
}
void traverse(TreeNode* root) {
if (root == nullptr) {
data += "#,";
return;
}
data += (to_string(root->val) + SEGMENT);
traverse(root->left);
traverse(root->right);
}
queue<string> split(string& str) {
queue<string> ans;
if (str.empty()) return ans;
int size = str.size();
int pre = 0;
for (int i = 0; i <= size; ++i) {
if (i == size || str[i] == SEGMENT) {
ans.emplace(str.substr(pre, i-pre));
pre = i+1;
}
}
return ans;
}
TreeNode* deserialize(queue<string>& data) {
if (data.empty()) return nullptr;
string first = data.front();
data.pop();
if (first == "#") return nullptr;
TreeNode* root = new TreeNode(stoi(first));
root->left = deserialize(data);
root->right = deserialize(data);
return root;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
// 这么做只是为了提前把字符串分开,如果放在后续代码中
// 会比较冗余
queue<string> iters = split(data);
return deserialize(iters);
}
};
cpp
/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<String> nodes){
if (nodes.isEmpty()) return null;
// 从后往前取出元素
String last = nodes.removeLast();
if (last.equals(NULL))
return null;
TreeNode root = new TreeNode(Integer.parseInt(last));
// 限构造右子树,后构造左子树
root.right = deserialize(nodes);
root.left = deserialize(nodes);
return root;
}
中序遍历:序列化可以,但是反序列化无解,因为根本找不到根节点啊!!!
层级遍历:剑指offer上的经典题型,层级框架如下:
cpp
voidtraverse(TreeNode root){
if (root == null)
return;
// 初始化队列,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
/* 层级遍历代码位置 */
System.out.println(root.val);
/*****************/
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null)
{
q.offer(cur.right);
}
}
}
## 235. 二叉搜索树的最近公共祖先








cpp
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode A = head;
ListNode B = head;
boolean f = false;
while(B != null && B.next != null){
A = A.next;
B = B.next.next;
if(A == B) {
f = true;
break;
}
}
if(!f) return null; //无环
A = head;
while(A != B){
A = A.next;
B = B.next;
}
return A;
}
}
## 剑指 Offer 22. 链表中倒数第k个节点
> 这道题不难,难的是细节,即到底有没有倒数K个节点? 即需要实现判断。
>
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
//遍历一遍?压入栈,而后出栈
//或是利用双指针,前者到达末尾节点则后者到达所需要的节点
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
if(head==NULL||k==0)//第一/二种情况
return NULL;
ListNode* before=head;
ListNode* behind=head;
for(int i=0;i<k-1;i++){
before=before->next;
if(before==NULL)
return NULL;
}
while(before->next!=NULL){
before=before->next;
behind=behind->next;
}
return behind;
}
};
## 876. 链表的中间结点
> 也是利用快慢指针,细节在于最后的判断,因为链表长度奇偶的不同,会造成中点选择的不同。
>
> 判断的依据就是快指针最后指向的是某个节点,还是null。或者直接返回慢指针!
>






<font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">vector</font><font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"><</font><font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">vector</font><font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"><</font><font style="color:rgb(170, 13, 145);background-color:rgb(247, 247, 247);">int</font><font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);">>> </font><font style="color:rgb(28, 0, 207);background-color:rgb(247, 247, 247);">dp</font><font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">(n, vector<</font><font style="color:rgb(170, 13, 145);background-color:rgb(247, 247, 247);">int</font><font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">>(target + </font><font style="color:rgb(28, 0, 207);background-color:rgb(247, 247, 247);">1</font><font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">, </font><font style="color:rgb(28, 0, 207);background-color:rgb(247, 247, 247);">0</font><font style="color:rgb(92, 38, 153);background-color:rgb(247, 247, 247);">))</font><font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);">;</font>
>
> 详见该题的链接。
>
> 拓展:背包问题的上述链接解法不仅包含了求最大值的方法,还有求最大值解法的商品情形,即回溯!
>
> 基本思想是利用状态转移方程的特点,反向推导之前的状态,从而确定下选用的商品。
>
















if(s[i]!=s[j]) <font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">dp[i][j] = min(dp[i + </font><font style="color:rgb(0, 128, 128);">1</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">][j], dp[i][j - </font><font style="color:rgb(0, 128, 128);">1</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">]) + </font><font style="color:rgb(0, 128, 128);">1</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">;</font>
> + 即需要做出选择,是先让i+1到j满足回文,还是让i到j-1回文,而后插入一个字符使得整个区间满足回文。
> + 这道题的遍历方式也值得推敲,需要从下到上,从左到右(因为转移方程的两个依赖项,i+1,j-1),详见:链接
>
> 思路拓展:
>
> + 相当于求最长的回文子序列(序列不是子串),构不成最长子序列的字符就需要插入相同的。比如mbadm,其中最长回文子序列为mam长度为3,因此需要插入5-3=2次,见答案:链接
> + 也相当于LCS问题(最长公共子序列),只不过是在一个字符串的头和尾进行比较,可以先看看最长公共子序列,再来想怎么解决这个问题:链接
>
## 55.游戏跳跃







cpp
//递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
## 23.合并K个升序链表
> 心路历程:
>
> 这道题是上一道题的进阶版本,难点在于如何选择下一个最小节点? 如果每次都去比,时间效率太低,而这种排序问题,可以直接使用堆排序来实现,在C++和Java中就是优先队列。
>
> 知识拓展:
>
> C++如何使用Priority_queue?对于普通类型,默认是大顶堆(普通类型使用greater


cpp
//DFS算法
//路径 选择 结束条件
class Solution {
private:
vector<vector<int>>res;
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<int>flag(nums.size(),0);
vector<int>temp;
help(nums,temp,flag);
return res;
}
void help(vector<int>&nums,vector<int>&temp,vector<int>&flag){
//满足条件返回
if(temp.size()==nums.size()){
res.push_back(temp);
return;
}
for (int i=0;i<nums.size();i++){
if(flag[i]==1){
continue;
}
flag[i]=1;
temp.push_back(nums[i]);
help(nums,temp,flag);
flag[i]=0;
temp.pop_back();
}
return;
}
};
## 八皇后(打印结果)
> 
cpp
class Solution {
public:
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) {
vector<int> pre;
dfs(n,0,pre);
return res;
}
void dfs(int n,int index,vector<int> &pre){
if(index==n){
// 这个思路很好啊,将其他的全部初始化为'.'
vector<string> ans(n,string(n,'.'));
for(int i=0;i<n;i++){
ans[i][pre[i]]='Q';
}
res.push_back(ans);
return;
}
for(int i=0;i<n;i++){
if(isValid(n,index,i,pre)){
pre.push_back(i);
dfs(n,index+1,pre);
pre.pop_back();
}
}
}
// 判断这个位置,能不放置皇后
bool isValid(int n,int index,int num,vector<int> &pre){
int prenum=pre.size();
for(int i=0;i<prenum;i++){
if(num==pre[i] || abs(num-pre[i])==index-i)//这个判断对角线的方式太秀了!!
return false;
}
return true;
}
};
## 52. N皇后 II

cpp
class Solution {
public:
int N;
int total=0;
int totalNQueens(int n) {
N=n;
vector<int> ans(n);
find(0,ans);
return total;
}
void find(int n,vector<int> &ans)
{
if(n==N)
{
//cout<<"1"<<endl;
total++;
return;
}
for(int i=0;i<N;i++)
{
int ok=1;
for(int j=0;j<n;j++)
{
//由于是按照行遍历,皇后只可能列相同
if(ans[j]==i || abs(ans[j]-i)==abs(j-n))//在一对角上或者在一行上
//其计算方式简单理解为:将上一个皇后的坐标行数减去当前行数
// 看是否与当前位置坐标列数减去当前列数相等
{
ok=0;
break;
}
}
if(ok)//没有冲突
{
ans[n]=i;
find(n+1,ans);
}
}
}
};
## 剑指 Offer 12. 矩阵中的路径

cpp
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
if(word.empty()) return false;
for(int i=0; i<board.size(); ++i)
{
for(int j=0; j<board[0].size(); ++j)
{
// 使用回溯法解题
if(dfs(board, word, i, j, 0)) return true;
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string& word, int i, int j, int w)
{
// 如果索引越界,或者值不匹配,返回false
if(i<0 || i>=board.size()
|| j<0 || j>=board[0].size() || board[i][j]!=word[w])
return false;
if(w == word.length() - 1) return true; //为什么是长度-1,因为是数组下标!从1开始的!
//暂存当前元素
char temp = board[i][j];
//这种方式可以实现原地判断,很方便
board[i][j] = '\0'; // 将当前元素标记为'\0',即一个不可能出现在word里的元素,表明当前元素不可再参与比较
if(dfs(board,word,i-1,j,w+1)
|| dfs(board,word,i+1,j,w+1)
|| dfs(board,word,i,j-1,w+1)
|| dfs(board,word,i,j+1,w+1))
{
// 当前元素的上下左右,如果有匹配到的,返回true
return true;
}
board[i][j] = temp; // 将当前元素恢复回其本身值
return false;
}
};
# 广度优先(BFS)
> 何时使用BFS?
>
> 从一个起点到达一个终点,求最短路径问题,有很大的空间开销,但是时间开销(平均/最好/最坏)是固定的。(其实是一种图的遍历)
>
> 核心思想是利用队列,遍历队列,将某一点周围的点放到队列中。
>
> 另外,在某些时候BFS比DFS更快,因为BFS是以迭代的方式全部并行进行,而DFS需要通过递归一个一个返回。
>
> 代码框架如下:
>
> 

## [752.打开转盘锁](https://leetcode-cn.com/problems/open-the-lock/)
> 
>
> **解题思路**:
>
> + 从题意上,**是求最短路径问题,可以考虑采用BFS算法**
> + 而后发现可选择的地方:**4个号码位,增加还是减少**
> + 所以基于BFS算法进行求解,当中途的号码命中死亡号码时,**说明不能进行这步操作直接continue**,放弃当前的选择;**否则,继续将最新的号码放入队列中,直到命中target;**
> + **注意:**为了减少计算重复的中间号码**,需要维护一个set,一旦之前已经出现了,就不再对其进行测试。**
> + 答案参考:[链接](https://leetcode-cn.com/problems/open-the-lock/solution/da-kai-zhuan-pan-suo-bfs-by-zxhnext-671m/)
> + **方法拓展**:在计算某一位上下移动的字符时,**可以利用这种方式简化计算:**`**<font style="color:rgb(170, 13, 145);background-color:rgb(247, 247, 247);">char</font>****<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"> a = (curr[j] -</font>****<font style="color:rgb(196, 26, 22);background-color:rgb(247, 247, 247);">'0'</font>****<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"> + </font>****<font style="color:rgb(28, 0, 207);background-color:rgb(247, 247, 247);">10</font>****<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"> + t) % </font>****<font style="color:rgb(28, 0, 207);background-color:rgb(247, 247, 247);">10</font>****<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"> + </font>****<font style="color:rgb(196, 26, 22);background-color:rgb(247, 247, 247);">'0'</font>****<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);">;</font>**`**,实现‘0’到’9‘的完美转换。**
>
cpp
class Solution {
public:
// 将 s[j] 向上拨动一次
string plusOne(string s, int j) {
if (s[j] == ‘9’)
s[j] = ‘0’;
else
s[j] += 1;
return s;
}
// 将 s[i] 向下拨动一次
string minusOne(string s, int j) {
if (s[j] == ‘0’)
s[j] = ‘9’;
else
s[j] -= 1;
return s;
}
// BFS 框架,打印出所有可能的密
int openLock(vector> **思路拓展**:
>
> 这道题**可以用双向BFS,同时从起点和终点开始遍历,并且每次查询自己的中间号码是否在对方的set中出现,一旦出现相同的,则说明已经找到咯!**
>
> 只在**知道起点和终点的情况下双向BFS算法,**不再使用队列**,而是用hash表或者set**,**这样可以快速判断两者间是否有交集**!
>
cpp
/class Solution {
public:
// 将 s[j] 向上拨动一次
string plusOne(string s, int j) {
if (s[j] == ‘9’)
s[j] = ‘0’;
else
s[j] += 1;
return s;
}
// 将 s[i] 向下拨动一次
string minusOne(string s, int j) {
if (s[j] == ‘0’)
s[j] = ‘9’;
else
s[j] -= 1;
return s;
}
// BFS 框架,打印出所有可能的密
int openLock(vector# 单调栈
> **什么时候使用单调栈?**
>
> + 比当前元素更大的下一个元素
> + 比当前元素更大的前一个元素
> + 比当前元素更小的下一个元素
> + 比当前元素更小的前一个元素
>
> 参考[链接](https://blog.csdn.net/qq_17550379/article/details/86519771)
>
## [739.每日温度](https://leetcode-cn.com/problems/daily-temperatures/)
> 
>
> 单调栈
>
> 解题思路:
>
> + 单调栈——往里面压入什么?压入坐标
> + 每次遍历到**新的温度时,和栈顶元素比较**,如果**比栈顶元素大**,那么根**据栈顶元素的下标就可以计算天数**,而后将其弹出,**继续比较栈顶,如此反复**
> + 而后**弹栈中所有元素,设为0**,答案参考:[739.每日温度](https://leetcode-cn.com/problems/daily-temperatures/)
>
## [496. 下一个更大元素 I](https://leetcode-cn.com/problems/next-greater-element-i/)
> 
>
> 单调栈;
>
> 解题思路:
>
> + 利用一个hashmap**保存nums1中出现的数以及对应的下标**
> + 利用单调栈,**遍历nums2,并利用上一个hashmap去查询对应的下标并保存**
> + 如果**hashmap中有该数,就压入栈中**
> + 答案参考:[链接](https://leetcode-cn.com/problems/next-greater-element-i/)
>
## [503. 下一个更大元素 II](https://leetcode-cn.com/problems/next-greater-element-ii/)
> 
>
> 单调栈;
>
> 解题思路:
>
> + 先用单调递减栈来保存数据,之前的流程跟普通单调栈一样
> + 当**遍历一遍数组之后,如果当前栈还有数据,那就再次遍历一遍数据**
> + 如果**当前栈还有数据,那就置位-1**
> + 一种将遍历长度设置为2n,下标为index=i%n,这样相当于将两次遍历合二为一,参考:[链接](https://leetcode-cn.com/problems/next-greater-element-ii/comments/92706)
>
## [42.接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)
> 
>
> 解题思路:
>
> + 这道题其实有两种思路:**一行一行的累加,或者一列一列的累加。**[链接](https://leetcode-cn.com/problems/trapping-rain-water/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-8/)
> + **行的累加**即求每一行的水
> + **列的累加**即求当前列可以存多少水
> + 列的方法本质上是**寻找某一点左右的最低边界,那么可存储的水就是最低边界与当前值的差值**
> + **暴力方式**:遍历每一点,并求左右的最高边界,时间复杂度O2n
> + **(简化方式:)动态规划**:分别从左从右遍历一次,取小标i的左右最大值,时间复杂度O(n)优化上述求点的过程,不能每次求两侧最大值的时候都使用遍历的方式,**因此采用dp数组来保存i的左右最大值,而后动态更新即可**
> + **双指针**:是对动态规划的空间压缩,**只使用两个变量来保存左右最大值**,过程极其巧妙[链接](https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/327718)
> + **单调栈**:单调递减栈,其实底层思路还是求某一个点两边的最大值,**不过需要乘以距离,因为有多格!**
> + **参考**[链接](https://leetcode-cn.com/problems/trapping-rain-water/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-8/)
>
cpp
class Solution {
public:
int trap(vector



1288.删除被覆盖的区间
解法:先根据开端进行排序,而后遍历,看两者的左右是否被覆盖。
这里只需要比较右边,因为左边已经按照大小进行排序了!
class Solution {
public:
int removeCoveredIntervals(vector<vector<int>>& intervals) {
int n = intervals.size();
sort(intervals.begin(), intervals.end(), [](const vector<int>& u, const vector<int>& v) { return u[0] < v[0] || (u[0] == v[0] && u[1] > v[1]);});
int ans = n;
int rmax = intervals[0][1];
for (int i = 1; i < n; ++i) {
//为什么只判断右边界?
//因为上一个区间的左边界必定小于下一个区间的左边界!
//因此只要右边界被包含,那么必定区间被覆盖
if (intervals[i][1] <= rmax) {
--ans;
}
else {
rmax = max(rmax, intervals[i][1]);
}
}
return ans;
}
};
56.合并区间
Ps:按照区间起点升序排列,开头就是最小的,结尾就是重合区间最大的。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
vector<int> tmp = {0, 0};//tmp第一个元素表示区间起始位置,第二元素表示终点位置
sort(intervals.begin(), intervals.end()); //这里排序会按照第一个数大小排,若大小相等再按照第二个数排
tmp = intervals[0]; //初始化第一个区间
for (int i = 1; i < intervals.size(); i ++ ) //遍历所有区间
{
if (tmp[1] < intervals[i][0]) //合并区间的右端点小于当前区间左端点,说明不能合并当前区间
{
res.push_back(tmp); //已经合并完一个区间
tmp = intervals[i]; //初始化新区间
}
else
{
tmp[1] = max(tmp[1], intervals[i][1]);//合并区间的右端点大于当前区间左端点,说明可以合并当前区间
} //这里合并只需要修改右端点即可,即右端点取最大值
}
res.push_back(tmp); //扫尾
return res;
}
};
986.区间列表的交集
57.插入区间
PS:这是从相反的方向解决问题;
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
int left = newInterval[0];
int right = newInterval[1];
bool placed = false;
vector<vector<int>> ans;
//只有这么三种情况:
//相交
//在左
//在右,依次判断即可
for (const auto& interval: intervals) {
if (interval[0] > right) {
// 在插入区间的右侧且无交集
if (!placed) {
ans.push_back({left, right});
placed = true;
}
ans.push_back(interval);
}
else if (interval[1] < left) {
// 在插入区间的左侧且无交集
ans.push_back(interval);
}
else {
// 与插入区间有交集,计算它们的并集
left = min(left, interval[0]);
right = max(right, interval[1]);
}
}
if (!placed) {
ans.push_back({left, right});
}
return ans;
}
2)回文串(字符串/链表)
思路:
判断字符串是否回文?直接两端往中心收缩,比较即可; 判断链表是否回文?要么新建一个反向链表而后一一比对,要么利用递归实现该功能(**利用二叉树后续遍历的思想实现链表的后续遍历**),见下:(tips:二叉树的前中后序遍历是如何实现的呢?迭代?递归?)这样实现的意义何在?其实就是利用递归,将链表的节点存储在栈中,而后一一取出即可(left=left.next就进行从头至尾的比较) PS:上述解法可以进一步压缩空间,先使用快慢指针寻找中点(**tips:如何寻找呢?快慢指针有什么用呢?(详见6.8)**),而后将中点之后的链表反转,比较头结点和中点开始的链表即可。
// 左侧指针
ListNode left;
booleanisPalindrome(ListNode head){
left = head;
return traverse(head);
}
booleantraverse(ListNode right){
if (right == null) return true;
boolean res = traverse(right.next);
// 后序遍历代码
res = res && (right.val == left.val);
left = left.next;
return res;
}
链接:
https://mp.weixin.qq.com/s/tCgEoOlZKS_ohuTx1VxJ-Q
409.最长回文串
回文串;动态规划;
解题思路:
- 中心扩张,选中任何一个为中点,向两边扩展,如果两边的字符相等,则继续扩展,否则,更新结果;
- 细节在于回文长度可能为奇数也可能为偶数,这就造成中心点的坐标选择问题,一是在选点的时候看i与i+1的字符是否相等来微调;二是参考这个答案:链接
516.最长回文子序列
解题思路:
- 跟求回文子串有很多相似的地方
- 这么来思考:假设已知dp[i+1]和dp[j-1],如何求dp[i][j]?很明显,跟s[i] s[j]有关
- 如果两者相等,那自然就是子问题+2;
- 如果两者不等,那就需要做出取舍s[i] s[j]不能同时位于一个子序列中,那个子区间最大,就取哪个,于是
<font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">dp[i][j] = max(dp[i + </font><font style="color:rgb(0, 128, 128);">1</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">][j], dp[i][j - </font><font style="color:rgb(0, 128, 128);">1</font><font style="color:rgb(51, 51, 51);background-color:rgb(248, 248, 248);">]);</font>
- 参考链接
234.回文链表
回文串;链表;
解题思路:
- 依次压入栈中,依次弹出比对
- 仿照二叉树后序遍历的思路,借助递归来实现上述栈的效果,参考链接
- 还有一种方式,利用快慢指针,找到中点,分割链表,而后反转中点之后的链表,这样就可以进行比对。
反转链表
反转链表,有两种通用解法:递归返回头结点;迭代两相背刺;
206. 反转链表
反转链表;
解题思路:
- 递归的方式:理解递归的重点就是明白递归函数的作用,比如反转链表以递归实现如下:
若是直接跳进递归的逻辑中,云山雾绕,很难理解。
- 想想 reverse作用是什么? 传入一个链表头节点,返回反转过后的头结点,并将这个新的头结点last,一直return回去。
- 迭代的方式:基本思路是保存当前节点的前后节点,并令当前节点的next指向其前节点(俗称,背刺!),依次背刺上一节点直到当前节点的next为null即可,返回当前节点。核心代码如下:
变种:反转链表前N个节点
其实在上一道题的基础上改一下就好。
解题思路:
- 首先遍历一遍,看看长度够不够N,不够直接返回失败;
- 递归的方式:增加一个全局变量,表示进入递归的次数,而后保存N个节点之后的第N+1个节点的指针,因为我们需要将两部分链表连接起来;
- 迭代的方式:同上,也是需要保存第N+1个节点的指针;
92. 反转链表 II
反转链表;递归
解题思路:
- 这道题,最终需要将情况细分到反转前N个链表上来
- 首先,reverseBetween(head,m,n),如果其中调用reverseBetween(head.next,m-1,n-1),那么如此递归下去,必将有m-1=1,这不就变成了反转前N个节点么?
仔细看看代码的递归妙用!
- 如果是迭代的方式,那么需要先遍历找到第m个节点,而后进行链表的反转,而后找到第n个节点,将反转过后的链表与之前的链表进行合并;
25. K 个一组翻转链表
反转链表;
解题思路:
- 第一种解法:递归,将K个一组反转链表,变为反转[a,a+k)区间的链表;参考链接
其中reverse(a,b)是以迭代的方式实现的;
- 第二种解法:迭代,
股票买卖
股票买卖的题型其实根本是动态规划。根据下面的题来由易到难来理解一下。
重点理解:dp数组的含义;状态转移方程;遍历的方向;有没有空间优化的可能。
labuladong的教程:链接
leetcode上的教程:链接
剑指offer 63.股票的最大利润(买卖股票的最大收益 |)
股票买卖;动态规划;暴力法
解题思路:
- dp[i][k],k取0/1,表示第i天,手头是否有股票,当前有多少钱(如果买入就是负数);
- 初始状态:dp[0][0]=0,表示第一天手头无股票,那就无收入;dp[0][1]=-prices[0],表示第一天买了股票,那么就是负收益;
- 那么状态转移方程就出来了:
- dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i])——第i天我手头没有股票,有两种可能,要么是我昨天就没买股票,要么就是今天眼疾手快脱手了。
- dp[i][1]=max(dp[i-1][1],-prices[i])——第i天我手头有股票,要么是昨天就买了,要么是今天才买了股票进场罚站。(且只能买入一手,故利润直接为-价格)
- 最后返回什么?dp[n-1][0],因为最后一天,肯定是手头没有股票了,才会有最大收益!
- 答案直接点题目链接,自己写过,如果想进行空间压缩,可以只保留前一天的数据
- 还有另外一种方式——暴力法:找某个元素之前的最小元素,这就是可能的最大值,可以通过记录之前的最小值,将复杂度降到O(N),参考链接
- 还可以使用一维的dp数组,表示前i天的最大收益,核心:前i天的最大收益 = max{前i-1天的最大收益,第i天的价格-前i-1天中的最小价格}
122. 买卖股票的最佳时机 II
动态规划;股票买卖;
解题思路:
- dp[i][k]的含义跟上一道题一样
- 初始状态也一样
- 状态转移有些区别:
- dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);
- dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i])——因为我们计算的是总共的利润,所以需要将之前的收入给加起来
- 贪心:如果今天的股价比昨天的股价高,那就加上差值
<font style="color:rgb(89, 89, 89);background-color:rgb(247, 247, 247);"> res += diff;</font>
LRU/LFU
LRU(Least Recently Used,最近最少使用),强调的是时间上的排序,即越久没有使用的缓存\页面进行删除\替换.
LFU(Least Frequency Used,最少频率使用),强调的是频率!当频率相同时,再比较时间!
LRU的解法
利用HashMap加双端链表,整个链表以时间来排序,.
hashmap用来获取链表节点,双端链表进行维护时间的顺序.
不能使用小顶堆,因为在get的时候,无法实现对堆内cache的更新和重排!
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity): capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode* node = new DLinkedNode(key, value);
// 添加进哈希表
cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
//很重要
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
//需要更新两个的节点
head->next->prev = node;
head->next = node;
}
//很重要
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
//很重要
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
//很重要
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
LFU的解法
可以使用Set来保证其frequency排序, 再用一个HashMap实现key到value的映射。
struct CacheNode {
int key;
int value;
int freq;
long tick;
// 小顶堆
bool operator<(const CacheNode& other) const {
if (freq < other.freq) {
return true;
} else if (freq == other.freq && tick < other.tick) {
return true;
}
return false;
}
};
class LFUCache {
public:
LFUCache(int _capacity) {
capacity = _capacity;
tick = 1;
}
int get(int key) {
auto iter = mp.find(key);
if (iter == mp.cend()) {
return -1;
}
int value = iter->second.value;
touch(iter->second);
return value;
}
void put(int key, int value) {
if (capacity == 0) {
return;
}
auto iter = mp.find(key);
if (iter != mp.cend()) {
// key exists, update value and touch
iter->second.value = value;
touch(iter->second);
return;
}
if (mp.size() == capacity) {
// Remove the first node in cache
const CacheNode& node = *cache.cbegin();
mp.erase(node.key);
cache.erase(node);
}
// Create a new node and save to set and map
CacheNode node{key, value, 1, tick++};
mp[node.key] = node;
cache.insert(node);
}
private:
long tick;
int capacity;
unordered_map<int, CacheNode> mp; // save the object instead of ptr
set<CacheNode> cache; // Balance BST
void touch(CacheNode& node) {
cache.erase(node);
node.freq++;
node.tick = tick++;
cache.insert(node);
}
};
滑动窗口
567.字符串的排列
滑动窗口;多指针;
5min mid
解题思路:
- 先遍历s1字符串,存储至hashtable1中;
- 建立两个指针left和right,分别表示左右边界;一个valid,表示有多少个字符满足了条件
- 新建一个hashtable2,遍历s2,并根据该字符是否在s1中,修改hashtable2中次数,当该字符在两个hash表中出现的次数相等时,valid+1;
- 开始进行左边界的收缩,条件为窗口大小大于s1.size。根据左边界的字符,对hashtable2以及valid进行修改
还有一种关于记录出现次数的方法,见到一个利用负数、0、正数,分别表示多出现了、刚好、还需要出现几次三种状态,使得代码很精简:链接
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int n=s1.size();
int m=s2.size();
if(n>m) return false;
vector<int>cnt1(26),cnt2(26);
//窗口为n的字符串 是否相同
for(int i=0;i<n;i++)
{
++cnt1[s1[i]-'a'];
++cnt2[s2[i]-'a'];
}
if(cnt1==cnt2) return true;
//
for(int i=n;i<m;i++)
{
//在数组右端添加字符
++cnt2[s2[i]-'a'];
//移除最左端字符
--cnt2[s2[i-n]-'a'];
if(cnt1==cnt2) return true;
}
return false;
}
};
作者:7aughing-6olickmiv
链接:https://leetcode.cn/problems/permutation-in-string/solution/hua-dong-chuang-kou-by-7aughing-6olickmi-95j9/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1358. 包含所有三种字符的子字符串数目
滑动窗口;多指针;
心路历程:
由于之前做过求字符串字典序的题,所以照着写,最后发现过不了最后的测试用例,原因在于左右边界循环次数太多,时间复杂度达O(n^2)。
因此必须使用滑动窗口来收缩范围。
滑动串口的精髓就在于,找到合适的右边界,而后不断的收缩左边界,直到条件不满足为止。
这个过程一般会利用hash表来进行存储相应的个数或者位置信息,如果忘了滑动窗口的过程,看这个滑动窗口详解。
知识补充:min_element和max_element可以找到容器中的最小最大元素的指针,详见:[链接](https://blog.csdn.net/hanshihao1336295654/article/details/82747931)
class Solution {
public:
int numberOfSubstrings(string s) {
int res = 0, l = 0;
unordered_map<char, int> m;
for (int i = 0; i < s.size(); i++) {
m[s[i]]++;
while (m['a'] > 0 && m['b'] > 0 && m['c'] > 0) {
res += s.size() - i;# 往后的序列都是啊!
m[s[l++]]--;
}
}
return res;
}
};
76.最小覆盖子串
滑动窗口;多指针;
10min hard
心路历程:
这道题是一道典型的滑动窗口题,难处理的是如何才算是满足条件?
这里的取巧方式是,s与t各有一个hash表,如果在遍历过程中两个hash表中某数字数量相等,则说明该字符达标了。当字符达标的数目达到字符的个数时,说明一个覆盖的子串已经找到了。而后收缩左边界,直到字符达标数目不在达标为止。
知识补充:利用substr成员函数可以直接从string取出指定位置长度的子串:[链接](https://baike.baidu.com/item/substr/2171?fr=aladdin)
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ?
"" : s.substr(start, len);
}
};
438. 找到字符串中所有字母异位词
滑动窗口;
秒了 mid
心路历程:就是上一道题的变种,只增加了索引的存储。由此可见,滑动窗口其实玩来玩去就是那样,重点在于抓住它的精髓。
3.无重复的最长子串
滑动窗口;
1min mid
心路历程:在判定条件上稍微卡了一会,后来发现只要新加入的字符串大于1就计算一次长度,而后收缩左边界,直到这个新元素为1为止,而后再次扩展右边界,如此循环。
//暂时AC
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
window[c]++;
// 判断左侧窗口是否要收缩
while (window[c] > 1) {
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
window[d]--;
}
// 在这里更新答案
res = max(res, right - left);
}
return res;
}
};
两数之和
框架:
双指针前后递进,大小判断,同时加上特殊条件的判定;
如下是基本的两数相加套路:vector<int> twoSum(vector<int>& nums, int target)
{
// 先对数组排序
sort(nums.begin(), nums.end());
// 左右指针
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if (sum < target) {
lo++;
} else if (sum > target) {
hi--;
} else if (sum == target) {
return {nums[lo], nums[hi]};
}
}
return {};
}
PS:有些题目中还可能遇到不只一对两数相加满足条件,求多组并且还要去重,这时可以用:
if (sum < target)
lo++;
else if (sum > target)
hi--;
else {
res.push_back({left, right});
// 跳过所有重复的元素
while (lo < hi && nums[lo] == left)
lo++;
while (lo < hi && nums[hi] == right)
hi--;
}
参考题目:PPS:有些题目中要求返回下标,这个时候sort就不行,可以考虑用暴力法,或是用hash表存储(target-nums[i]),而后看后续是否有数字满足;
1.两数之和
PS:这道题是复用了两数之和的函数而实现的;
PS2:这道题所谓的双指针其实就是将两数之和的函数给内置到了三数之和中;
区间合并与分割
56.合并区间
注:匿名函数的使用,sort是容器函数,需要借助匿名函数进行自定义的比较。
还可以这么写:
57.插入区间
注:place表示是否已经将改区间给放进去了,如果放进去了,那么在右边的直接往里边丢就行。
大数计算
大数计算的题型,一般计算的整数都超过Long Long的范围了,所以需要字符串或者容器来作为中间介质。
核心计算逻辑就是这个:
66. 加一
大数计算;
解题思路:
- 从后往前进行计算,取余数为当前位数,取商为进位;
- 如果最后一位计算之后,还有进位,那就往前插入一个1;
- 也可以转换成大数相加的方式,将1转换为“0000001”进行相加;
- 参考链接
2. 两数相加
大数计算;链表;多指针
解题思路:
- 两个指针,分别指向两个链表的开始位置
- 模拟相加的过程,一位一位的向后传递进位
- 如果某一个链表遍历完毕,那就只遍历另外一个链表即可
- 思考一下,如果链表不是逆序的,应该怎么处理?——只能先手动反转链表了
参考链接:链接
43. 字符串相乘
大数计算;字符串
解题思路:
- 我的解法,模拟乘法的过程,借助双指针,让某一个字符串的所有数与另外一个字符串的位依次相乘
- 在计算的时候需要保留进位,最后再相加
- 这种方式,相当于还要实现一遍字符串的相加,而后在过程中调用对应的函数
- 更便捷的解法,我们将乘法的计算过程进一步拆分一下
- 由上图可知,两个字符串i j位置的数相乘,那么结果只会影响到i+j 和i+j+1两位,其中i+j+1保存的是进位
- 那么我们完全可以遍历字符串,而后取位数相乘,将对应的位置存储于某一个数组中
- 而后将数组中的结果连接成字符串,结果不就出来了?
- 值得注意的是,以这种方式进行计算的时候,也需要从后往前进行遍历相乘,因为低位向高位有进位
- 同时注意进位,看这个答案:链接
- 参考链接:链接
数据结构
数据结构类型的题,是指需要理解各种常用的数据结构及算法,并通过算法来实现它
剑指offer09.用两个栈实现队列
数据结构;栈;队列
解题思路:
- 队列是先入先出,栈是先入先出,那么如果两个栈相互“串联”就可以模拟出先入先出了!
- 于是解法出来了,两个栈中一个负责接收“入队”,一个负责处理“出队”
- 当入队时,元素全部push进入第一个栈
- 当出队时,如果第二栈为空,那就将第一个栈的元素依次弹出pop并压入第二栈中,而后对第二个栈进行弹出
- 这样的时间复杂度,平均下来就是O(1)。
- 拓展:如何获取队首?其实可以对第二栈进行top访问,不进行弹出即可;
225.用两个队列实现栈
栈;队列;数据结构
解题思路:
- 两队列,一个负责接收入队push,另一个负责处理出队时的暂存区
- 入队时,数据全部放到第一队
- 出队时,将第一队的数依次出队放到第二队,只剩最后一个元素
- 而后将第二队的数据又重新赋值给第一队(这个部分本想用一个指针来指示,但是队列没有这样的操作),清空第二队
- 如此反复,时间复杂度较高,出队O(n),入队O(1)
- 拓展:其实用一个队列就可以了!在一个队列时,出队n-1个数据,并且依次再次入队,只需要返回第n个数据即可!这里有一道题解是入队O(n),出队O(1)
71. 简化路径
这道题建议直接将‘/’略过不做考虑!
而后将栈中的结果出栈,用‘/’连接在一起,值得注意的是,在C++中可以使用vector模拟栈,并且最后的结果还不用倒置。
图的奥秘
有关图的遍历与拓扑排序等;
先解决一个问题:如何存储图?
最简单的方式是利用一个二维数组来存储,如下:
图的遍历
以城市之间的距离为例,DFS和BFS都可以用作寻找最短路径。
如果是BFS,需要是队列来进行图的遍历并更新最短距离,其实BFS还可以用作求最少转机次数。将转机次数放在queue的结构体里面,也是一个很便捷的方法。
时间复杂度:O(N),空间复杂度为O(N^2);
因为DFS/BFS都需要遍历所有的点。
如果是任意两点之间的求最短路径,则需要每两个点进行一次BFS或DFS,即N^2次BFS和DFS。
Floyd-Warshall算法求最短路径
Floyd算法的基本思想就是对将各个顶点作为中转站,从而更新最短路径。
这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。
它的时间复杂度是O(N^3),在数据量不是很大的情况下有奇效。
这其实是一种动态规划的思想。
另外,如果图中有不可达的点,可以这样处理:
有个问题,那就是Floyd算法无法解决“负权回路”的图,因为每次经过负权的点,都会更新路径,并且如果一个图存在负权回路,那么也就不存在最短路径。
743.网络的延迟时间
最短路径;Floyd算法;
解题思路:
- 这道题完全就是Floyd算法的典型题;
Dijkstra(**迪杰斯特拉**)算法求单源最短路径
何为单源最短路径?
即是求指定一个源点到其余各个顶点的最短路径。例如求下面1号顶点到2、3、4、5、6号节点的最短路径。
Dijstra算法的主要思想是:通过“边”来松弛(某节点1通过某相邻节点2中转,使得它到节点3的距离变短)各个节点到其他节点的距离。
以一号节点为例,如下是其初始路程:用一个一维数组dis来存储1号顶点到其余各个顶点的初始路程,如下:
将上述数组为估计值,因为后续需要做的工作就是对其更新。
我们看到1->2的路径最短,且1->2的过程不可能再短了(1到其他节点的距离都是正数,且到2是最短,总不可能还有通过其他节点中转使得1->2路径缩短吧?)
然后我们看到2号节点有“估计值”如下:
那么1号节点就可以通过2号节点来“松弛”1->3,1->4的路径,这时更新为:
接下来要做的就是从3,4,5,6(2已经走过了)找距离最小的那个节点进行中转,比如4号节点,再次更新;再从3,5,6选最近节点更新···如此反复,最终如下:
上述算法的基本思想就是:每次找到离源点(源点如1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。
Dijlstra算法基本步骤如下:
- 顶点分为两个部分:已知最短路径P和未知最短路径Q。
最开始时P只有源点(如题目的1),其他全都在Q。这个可以用一个flag数组保存,如果flag[i]==1,那句表示在P,否则在Q。
- P中源点设置dis到自身的距离为0,其他设置为边的长度即可;
- 在Q中选一个离源点最近的点对源点的dis进行松弛,并更新相应的距离;
- 将第3步所选的点从Q中删除,Q为空则算法结束;
实例代码如下:
上述算法的时间复杂度为O(N^2),因为O(N)找到最近点,而后O(N)更新最近的距离。
其实是基于贪心策略的算法。
如何优化?
首先,使用堆排序来找最近的点,O(logn);而后,对于边数M远小于N^2的稀疏图来说,可以将邻接矩阵改成邻接表,邻接表怎么存储?例如使用数组来实现邻接表(还可以使用指针链表):
拓扑排序
207.课程表
图;拓扑排序;
解题思路:
顶点的度是指和该顶点相连的边的条数。特别是对于有向图来说,顶点的出边条数称为该顶点的出度,顶点的入边条数称为该顶点的入度,度=出度+入度;
有向无环图→ 一定存在入度为0的点,不然就没法解题了;
拓扑排序简单来说,是对于一张有向图 G,我们需要将 G 的 n 个点排列成一组序列,使得图中任意一对顶点 ,如果图中存在一条 u→v 的边,那么 u 在序列中需要出现在 v 的前面。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。
拓扑排序可以使用BFS与DFS来做,其中BFS较容易理解,主要是以下几个步骤:
- 让入度为 0 的点入列,它们无前置。
- 然后逐个出列,出列代表该节点已排序,减小其下个节点的入度。
- 如果下个节点的入度新变为 0,安排它入列、再出列……直到没有入度为 0 的节点可入列。
- 一般来说,出队次数必然节点数,否则就是有环。
1203.项目管理(拓扑排序)
图;拓扑排序;
- 解题思路:
- 相比于普通的拓扑排序,这道题的差异在于点之间有分组,即涉及到组间的排序
- 于是这道题分为两个步骤:第一是完成组与组的依赖关系,看是否存在一个拓扑排序,如果两个组之间相互依赖,任务会停摆;
- 第二是完成组内的排序;
- 其中,组与组以及组内的排序,其实都是拓扑排序,将组中所有的项目都抽象成一个点即可;
- 题解参考:链接
多线程
交替打印字符串(原子、条变量、锁)
法一、利用一个原子变量bool来判断,打印出来之后就取反,而后另外一个线程开始打印,如此反复;
class FooBar {
private:
int n;
mutex mtx;
condition_variable cv;
bool foo_done = false;
public:
FooBar(int n) : n(n) {}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&]() { return !foo_done; });
printFoo();
foo_done = true;
cv.notify_one();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&]() { return foo_done; });
printBar();
foo_done = false;
cv.notify_one();
}
}
};
法三、信号量,初始化两个信号量,一个为1一个为0(1的就是先打印的),打印之后相反的信号量加一(post)

cpp
class FooBar {
private:
int n;
std::mutex m1,m2;
public:
FooBar(int n) {
this->n = n;
m2.lock();
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
m1.lock();
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
m2.unlock();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
m2.lock();
// printBar() outputs "bar". Do not change or remove this line.
printBar();
m1.unlock();
}
}
};
补充:
std::this_thread::yield() 的目的是避免一个线程频繁与其他线程争抢CPU时间片, 从而导致多线程处理性能下降.
std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片(给其他线程使用)
std::this_thread::sleep_for() 是让当前休眠”指定的一段”时间.
sleep_for()也可以起到 std::this_thread::yield()相似的作用, 但两者的使用目的是大不相同的。
条件变量
并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。
虽然并查集使用BFS/DFS在某些时候也是可以解题的,但是通用和简洁的解法是联合-查找算法(Union-find Algorithm)(该算法后面时间复杂度趋近于O(1)),该算法定义了两个用于此数据结构的操作:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。即寻找到它的根节点
- Union:将两个子集合并成同一个集合。即将某一条路径上的点的最终父节点设置为另一条路径的最终父节点。如下,如果想要merge(8,5),那么过程如下:
- 上面这种合并的方式,在路径太长的时候,耗时较多,就像是一个链表在遍历,因此可以对其进行优化:直接将路径上的点指向最终父节点,而不是一级一级的找。
由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(Union-find Data Structure)或合并-查找集合(Merge-find Set)。
进一步了解推荐阅读:一文搞懂并查集:原文链接
并查集
547. 省份数量
解题思路:
- 并查集的典型解法
- 首先以一个二维矩阵来表示省份之间的链接关系,以N表示初始时的省份数量
- 而后运用并查集的方式进行合并,何时需要合并?如果M[i][j]==1,就表示需要合并,如果merge(i,j)成功,N—;如果合并不成功,说明已经合并过了,那么N不变
- 最后的结果就是省份的数量
- DFS/BFS解法,查看:链接
std::vector<int> fa;
void init(int N) {
fa.resize(N);
for (int i = 0; i < N; i++) {
fa[i] = i;//对应自身
}
}
//返回 x 所属集合的ID
int find(int x) {
int r = x;
while(fa[r] != r) {
r = fa[r];
}
//进行路径压缩
while(fa[x] != x) {
int t = fa[x];
fa[x] = r;
x = t;
}
return x;
}
bool merge(int u, int v) {
int ru = find(u);
int rv = find(v);
if (ru == rv) {
return false;
}
fa[ru] = rv;
return true;
}
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
int N = M.size();
init(N);
int anw = N;
for (int i = 0; i < N; i++) {
// i+1是因为之前的已经被合并过了
for (int j = i+1; j < N; j++) {
if (M[i][j] && merge(i, j)) {
anw --;
}
}
}
return anw;
}
};
200.岛屿的数量
解题思路:
- 有三中那个思路,一种是这章的重点——并查集,另一种方法是利用BFS;还有一种方式是利用DFS
- 如果并查集的思路,怎么处理合并问题?如何表示一个二维矩阵的坐标呢?可以使用icols+j,相当于从第一列开始数的顺序。那么如何处理这里所谓的链接?之前省份的那道题是给出了链接关系,而岛屿的链接问题,可以以向右和向下进行判断,是否有岛屿,从而进行合并,**在遍历的过程中记录空地 0 的数量,那么最终的岛屿数量就是colsrows-空地的数量-剩余联通分量的数量。(联通分量就是说合并了多少次岛屿**)
- 如果是DFS,一般是利用栈或者递归的方式实现,递归的方式更简洁。先建立一个visited二维数组,表示所有的点都没有访问过,DFS的精髓,就是从一个点出发,依次进行上下左右的访问,会将途中访问过的点进行标记,下一次就不会对其进行访问了。
- 如果是BFS,一般是利用队列来迭代实现,只要队列不为空,那就继续进行扩展。思路跟DFS一样,利用一个二维的visited数组表示该点有没有被访问过,如果被访问/或者是水那就下一个点。
- 三种答案建议参考链接
1319. 连通网络的操作次数
并查集;
解题思路:
- 剥开外壳,这就是一个规规矩矩的并查集题
- 首先,通过并查集,我们是不是可以获得当前网络连接中的“子网个数”?
- 如何才能让所有子网互通? 那就在子网之间两两加上一根线,一共需要 子网数量-1 的线
- 于是,线够不够就是所有子网能不能联通的关键。那么到底有多少根线可以使用?
- 在并查集合并的过程中,如果发现两个电脑其实已经是互通的了,那么这根线就是多余的!
- 多余的线数量>=需要的线数量,就能够合并 可以参考:链接
剑指 Offer II 116. 朋友圈
并查集;DFS;BFS
解题思路:
类似题型
改成了初始全部为水,而后将对应位置的水置为1的过程;
这道题的方法很多,可以使用先sort再滑动窗口(用桶排序,不推荐)、set(时间复杂度略高)、unordered_set(速度快,因为不用往上下都遍历)、并查集(都合并到最小的那个数,同时用一个unordered_map来存储个数)参考链接