刷题顺序: 数组 ->字符串 ->链表->二分查找->排序->哈希表-> 栈->队列 ->树 、递归、回溯 -> 堆
刷题网站:https://programmercarl.com/
数据结构相关
基础的数据结构:数组、字符串、树、堆、栈、队列、哈希表
数组
数组是存放在连续内存空间上的相同类型数据的集合。
一维数组在内存空间的地址是连续的,而二维数组在内存中是存放各个一维数组的首地址的一维数组,再指向各个一维数组的连续内存地址。
题:
35. 搜索插入位置
(二分)
27. 移除元素
(同26. 删除排序数组中的重复项)双指针
209. 长度最小的子数组
滑动窗口(类似于双指针)
59. 螺旋矩阵 II
注意循环边界
链表
链表节点的定义
1 | // 单链表 |
C++默认生成一个构造函数,但是这个构造函数不会初始化任何成员变化(在初始化的时候就不能直接给变量赋值)。在C++里最好是手动释放内存
题:
203. 移除链表元素
(设置虚拟头节点,这样避免头节点的特殊处理~)
707. 设计链表
(链表的增删查改)
206. 反转链表
(改变指针的指向)
142. 环形链表 II
(快慢指针)
有关于慢指针进入环的第一圈就一定会和快指针相遇的解释,也可这么理解:从慢指针进入环(环长为m)那一刻开始算,假设相遇时,慢指针走了x,快指针则为2x。x若大于等于m/2,那快指针一定遇上了慢指针。(也可以理解为物理里面的相对运动~~)
哈希表
(牺牲空间换时间)
「一般来说哈希表都是用来快速判断一个元素是否出现集合里」。
对于哈希表,要知道「哈希函数」和「哈希碰撞」在哈希表中的作用.
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法(链表)和线性探测法(移动到空位)。
哈希法的数据结构
数组
set (集合)
map(映射)
C++
红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
set 与 map 的异同:
均为关联式容器,有序的key 均以RBTree作为底层容器 。
但set所得元素的只有key没有value,value就是key ,不能通过迭代器来改变set的值;而map所有元素都是键+值存在 , map的键是不能修改的,但是其键对应的值是可以修改的.
题:
242. 有效的字母异位词
数组哈希(比map空间消耗要少,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。)
349. 两个数组的交集
set哈希 (set与vector的相互转换)
1 | vector<int> nums1 |
无法使用数组来做哈希表了。
「主要因为如下两点:」
- 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
- 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
202. 快乐数
(所谓“无限循环”,unordered_set来判断一个数是否重复出现过)
1. 两数之和
(利用map的key-value特性)
1 | unordered_map<int, int> map; |
15. 三数之和
18. 四数之和
(15,18都有难度,和二数之和不太一样,双指针更好一些)
454. 四数相加 II
(只需要求次数,而且可重复,因此~用map)
字符串
字符串与数组的区别
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组.
在C语言中,把一个字符串存入一个数组时,也把结束符 ‘\0’存入数组,并以此作为该字符串是否结束的标志。
在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束
vector< char > 和 string: 在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
题:
344. 反转字符串
541. 反转字符串 II
剑指 Offer 05. 替换空格
151. 翻转字符串里的单词
(先整体反转,再局部反转)
1 | 方法一:双指针+栈 时间复杂度 O(n),空间复杂度 O(n) |
1 | 方法二:流+栈 时间复杂度 O(n),空间复杂度 O(n) |
1 | 方法三: |
剑指 Offer 58 - II. 左旋转字符串
(先局部反转再整体反转.)
1 | 方法一:栈 |
1 | 方法二: |
KMP算法
时间O(m+n),空间O(m)
主要解决字符串匹配.重复子串问题
KMP的经典思想就是:「当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。」
核心:前缀表
(前缀包含首字母,不包含尾字母的子串;后缀则是不包含首字母,包含尾字母的子串)
定义大体是这样: 设S为非空字符串。若字符串A=BS,则B是A的前缀子串;若A=SB,B是A的后缀子串。比如aabaa:
前缀 | 后缀 |
---|---|
a | a |
aa | aa |
aab | baa |
aaba | abaa |
「前缀表是用来回溯的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。」
前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
寻找最长相等前后缀(前缀表存放的值)
i 后缀末尾, j前缀末尾;
j也代表了i包括i之前子串的最长相等前后缀.
28. 实现 strStr()
1 | class Solution { |
459. 重复的子字符串
1 | // KMP |
栈和队列(C++)
C++中stack 是容器么?
不是,是container adapter(容器适配器)【同队列】
我们使用的stack是属于那个版本的STL?
SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
我们使用的STL中stack是如何实现的?
栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
「栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。」
「我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。」deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。「SGI STL中 队列底层实现缺省情况下一样使用deque实现的。」
1
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
1
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
stack 提供迭代器来遍历stack空间么?
不允许有遍历行为,不提供
题:
232. 用栈实现队列
225. 用队列实现栈
(一个栈: 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部)
20. 有效的括号
由于栈结构的特殊性,非常适合做对称匹配类的题目
1047. 删除字符串中的所有相邻重复项
1 | // 原地操作 |
1 | // 字符串作栈 |
1 | // 反向入栈 |
150. 逆波兰表达式求值—有一点小问题
在C++中是不能对字符串std::string使用switch/case语句的,把string 做一下hash处理,变成一个int数.
如何哈希
-
相同点:
①都是C++的字符处理函数,把数字字符串转换成int输出
②头文件都是#include
不同点:
①atoi()的参数是 const char ,因此对于一个字符串str我们必须调用 c_str()的方法把这个string转换成 const char类型的,而stoi()的参数是const string,不需要转化为 const char;②stoi()会做范围检查,默认范围是在int的范围内的,如果超出范围的话则会runtime error!
而atoi()不会做范围检查,如果超出范围的话,超出上界,则输出上界,超出下界,则输出下界;
1 | class Solution { |
239. 滑动窗口最大值
1 | class Solution { |
347. 前 K 个高频元素—有一点小问题
如何手写堆,优先队列.:https://leetcode-cn.com/problems/top-k-frequent-elements/solution/python-dui-pai-xu-by-xxinjiee/
小顶堆,因为要统计最大前k个元素; 大顶堆,因为要统计最小前k个元素.
可以这么理解:(小顶堆求最大TOPK)堆(size = k)里存放的是当前元素的频率,其中堆顶的元素最小。加入新进来的元素,此时若size>k,弹出堆顶元素,调整堆。【大顶堆求最小TOPK类似】
1 | //利用 multimap |
1 | // 优先级队列 「就是一个披着队列外衣的堆」,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。 |
NC119.最小的K个数
大根堆:首先利用前k个数构建“大顶堆”,剩余元素依次与堆顶元素相比较。
如果大于堆顶元素,则不做处理
如果小于堆顶元素,则将堆顶元素与这个元素交换,然后再恢复堆序。
这样的话最后这个堆里面保存的就是最小的k个数(无序),可以通过。
1 | // 大顶堆 a[i]< a[2i+1] a[i]<a[2i+2] |
NC88 寻找第K大
1 | // 小顶堆 a[i]> a[2i+1] a[i]>a[2i+2] |
NC97 出现次数的TopK问题
1 |
|
二叉树
二叉树的种类
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
满二叉树【国内定义】:如果一棵二叉树只有度为0的结点(叶子节点)和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。也可以说深度为k,有2^k-1个节点的二叉树。【国外定义:满二叉树就是没有度为1的节点】
完全二叉树: 在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点.
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
二叉搜索树:二叉搜索树是有数值的了,「二叉搜索树是一个有序树」。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
「C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树」,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表,时间复杂度是o(1)。
二叉树的存储方式
「二叉树可以链式存储(指针),也可以顺序存储(数组)。」
「如果父节点的数组下表是i,那么它的左孩子就是i * 2 + 1,右孩子就是 i * 2 + 2。」
二叉树的遍历方式
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
「这两种遍历是图论中最基本的两种遍历方式」
深度优先遍历(前中后其实指的就是中间节点的遍历顺序)
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。
广度优先遍历
层次遍历(迭代法)
一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
1 | struct TreeNode { |
二叉树的定义
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
- 求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)
问题
平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?
是的,是二叉搜索树和平衡二叉树的结合。
平衡二叉树与完全二叉树的区别在于底层节点的位置?
是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
堆是一棵完全二叉树,同时保证父节点一定>=子节点的顺序关系(有序)。「完全二叉树一定是平衡二叉树,搜索树是中大于左小于右,堆不是平衡二叉搜索树」
递归三要素
- 「确定递归函数的参数和返回值:」确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 「确定终止条件:」写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 「确定单层递归的逻辑:」确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
题:
144. 二叉树的前序遍历
94. 二叉树的中序遍历
145. 二叉树的后序遍历–Morris 遍历(面试几乎不会考)
1 | 递归版 |
1 | 迭代版 |
102. 二叉树的层序遍历
在 BFS 遍历的基础上区分遍历的每一层,就得到了层序遍历。在层序遍历的基础上记录层数,就得到了最短路径.
此时队列中的结点是 3、4、5,分别来自第 1 层和第 2 层。这个时候,第 1 层的结点还没出完,第 2 层的结点就进来了,而且两层的结点在队列中紧挨在一起,我们无法区分队列中的结点来自哪一层。
因此,在每一层遍历开始前,先记录队列中的结点数量 n(也就是这一层的结点数量),然后一口气处理完这一层的 n 个结点。
1 | class Solution { |
层序遍历的一些变种题目
199. 二叉树的右视图 DFS 与 BFS的运用
589. N 叉树的前序遍历 有用到C++ STL 反向迭代器适配器(reverse_iterator)详解
LeetCode 103. Binary Tree Zigzag Level Order Traversal 之字形层序遍历
LeetCode 515. Find Largest Value in Each Tree Row 计算每一层的最大值
对于最短路径问题,还有两道题目也是求网格结构中的最短路径,和我们讲解的距离岛屿的最远距离非常类似:
LeetCode 542. 01 Matrix
LeetCode 994. Rotting Oranges
还有一道在真正的图结构中求最短路径的问题:
LeetCode 310. Minimum Height Trees
作者:nettee
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
来源:力扣(LeetCode)
226. 翻转二叉树
关键点在于 每个节点遍历一次
———-遍历————
101. 对称二叉树
104. 二叉树的最大深度
111. 二叉树的最小深度
222. 完全二叉树的节点个数
110. 平衡二叉树
257. 二叉树的所有路径
404. 左叶子之和
513. 找树左下角的值
如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!;后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值
112. 路径总和
———-属性————
106. 从中序与后序遍历序列构造二叉树
1 | // 分解成一个子问题:找到中间节点,左右子树的中序&&后序, 每层递归定定义了新的vector(就是数组),既耗时又耗空间 |
1 | // 优化版 |
1 | //再次优化版:因为后序是左右中,反过来就需要先构建完右子树再构建左子树 |
105. 从前序与中序遍历序列构造二叉树
「前序和后序不能唯一确定一颗二叉树!」,因为没有中序遍历无法确定左右部分,也就是无法分割。
1 | //沿用上一题的方法三思路 |
654. 最大二叉树
617. 合并二叉树
——修改与构造——-
700. 二叉搜索树中的搜索
98. 验证二叉搜索树
特性:中序遍历下,输出的二叉搜索树节点的数值是有序(递增)序列
递归踩雷:左子树所有节点小于中间节点,右子树所有节点大于中间节点
1 | //递归版 |
1 | //迭代版 |
530. 二叉搜索树的最小绝对差
在有序数组求任意两数最小值差等价于相邻两数的最小值差
501. 二叉搜索树中的众数
236. 二叉树的最近公共祖先
1 | //自底向上查找 (后序遍历-回溯) |
235. 二叉搜索树的最近公共祖先
只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先
1 | class Solution { |
——-二叉搜索树——
701. 二叉搜索树中的插入操作
450. 删除二叉搜索树中的节点
1 | class Solution { |
1 | // 根据递归返回值 确定节点的父子关系 (自己理解后重新写的) |
669. 修剪二叉搜索树
1 | //递归版 |
1 | // 迭代版 |
108. 将有序数组转换为二叉搜索树
538. 把二叉搜索树转换为累加树
二叉搜索树的修改与构造
算法思想
基础的算法: 枚举遍历,排序, 二分查找,递归,回溯
回溯
「因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案」,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
适合: (性能分析)
子集问题:一个N个数的集合里有多少符合条件的子集
- 时间复杂度:O(n * 2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n),构造每一组子集都需要填进数组,又有需要O(n),最终时间复杂度:O(n * 2^n)
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
组合问题:N个数里面按一定规则找出k个数的集合
- 时间复杂度:O(n * 2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。
排列问题:N个数按一定规则全排列,有几种排列方式
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n n-1 n-2 * ….. 1 = n!。
- 空间复杂度:O(n),和子集问题同理。
切割问题:一个字符串按一定规则有几种切割方式
棋盘问题:N皇后,解数独等等
N皇后
- 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n (n-1) …. * 1。
- 空间复杂度:O(n),和子集问题同理。
解数独
时间复杂度:O(9^m) , m是’.’的数目。
空间复杂度:O(n^2),递归的深度是n^2
组合问题
组合问题、切割问题和子集问题都是类似的问题,可转换成组合问题
如果是一个集合来求组合的话,就需要startIndex,例如:回溯算法:求组合问题!,回溯算法:求组合总和!。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:回溯算法:电话号码的字母组合
77. 组合
方法一:经典的回溯剪枝-满足题目要求的k个元素
官网方法二:简单来说就是一开始把所有的1放在尾部,比如00111这样,然后按顺序把1往前移动。如果所有的1都在最前头了,比如11100,就留一位1,其余的全部移到末尾,比如10011,然后再重复这个操作,直到所有的1都“被”留在了最前面,即11100,便完成了全部穷举。
1 | // 二进制法 |
216. 组合总和 III
17. 电话号码的字母组合
回溯法有点像(递归有几种类型,循环是第i个类型有几种选择)
「for循环横向遍历,递归纵向遍历,回溯不断调整结果集」。
39. 组合总和
40. 组合总和 II
1 | class Solution { |
131. 分割回文串
93. 复原 IP 地址
78. 子集
「在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果」。
90. 子集 II
通过排序,比较相邻重复元素是否被访问,达到去重.
491. 递增子序列
(回溯套路小陷阱)
本层去重,是指同一个父节点下的本层,而不是整颗树的本层。
父节点的同一层公用一个visit[201]数组(哈希),完成访问登记,
排列问题
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
“树层去重”和“树枝去重”的重要性, 明显看出“树层去重”效率更高(「可以看出在candidates[i] == candidates[i - 1]相同的情况下:」)
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
46. 全排列
47. 全排列 II
—-未做—-
332. 重新安排行程
图论扩展
51. N 皇后
棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里
37. 解数独
「二维递归」
贪心
「贪心的本质是选择每一阶段的局部最优,从而达到全局最优」
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
题:
455. 分发饼干
376. 摆动序列
让序列有尽可能多的局部峰值。
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
53. 最大子序和
贪心:「不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数」
1 | int maxsumofSubarray(vector<int>& arr) { |
动态规划:arr[i]代表i的累加和,便记录其累加和最大值
动态转移方程:arr[i] = max(arr[i-1]+arr[i] ,arr[i])
1 | int maxsumofSubarray(vector<int>& arr) { |
122. 买卖股票的最佳时机 II
「局部最优:收集每天的正利润,全局最优:求得最大利润」
55. 跳跃游戏
45. 跳跃游戏 II
1005. K 次取反后最大化的数组和
134. 加油站
135. 分发糖果
需要考虑两个维度(先考虑其中一个维度,再考虑其他的)
860. 柠檬水找零
406.根据身高重建队列
注意 数据结构的区别,vector的底层实现也是普通数组(使用vector来做insert的操作,此时「虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t * n)级别的,t是底层拷贝的次数」)
动态规划
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的.
核心: 状态转移公式(递推公式)
对于动态规划问题,如下五步曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
题:
509. 斐波那契数
70. 爬楼梯
746. 使用最小花费爬楼梯
62. 不同路径
343. 整数拆分
96. 不同的二叉搜索树
——子序列系列——-
674. 最长连续递增序列
300. 最长递增子序列
该题与674的区别在于子序列的元素可以在位置上不连续。
1 | int lengthOfLIS(vector<int>& nums) { |
优化版
二分区间的选择(来源):
while (left < right)
退出循环的时候有left == right
成立,因此无需考虑返回left
还是right
每一轮区间被划分成 2 部分,理解 区间划分 决定中间数取法( 无需记忆,需要练习 + 理解 ),在调试的过程中理解 区间和中间数划分的配对关系:
划分 [left, mid] 与 [mid + 1, right] ,mid 被分到左边,对应 int mid = left + (right - left) / 2;;
划分 [left, mid - 1] 与 [mid, right] ,mid 被分到右边,对应 int mid = left + (right - left + 1) / 2;。至于为什么划分是这种对应关系,原因在于区间只有 2 个数的时候,如果中间数的取法不对,一旦进入的分支不能使得区间缩小,会出现 死循环。
退出循环的时候有 left == right 成立,此时如果能确定问题一定有解,返回 left 即可,如果不能确定,需要单独判断一次。
1 | //二分+动态规划 |
718. 最长重复子数组
1143. 最长公共子序列
与上一题相比,子序列(原字符串元素相对顺序不变,删除某些字符(也可以不删除任何字符)后组成的新字符串),难度加大.还是要理清dp[i][j]
所代表的含义以及对应的递推公式、初始值.
1 | //优化版,只保留相邻两行的状态,空间复杂度降低为O(min(m,n)) |
——01背包——-
416. 分割等和子集
其他
42. 接雨水
来源:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/精选评论第一条
先明确几个变量的意思:
1 | left_max:左边的最大值,它是从左往右遍历找到的 |
定理一:在某个位置i
处,它能存的水,取决于它左右两边的最大值中较小的一个。
定理二:当我们从左往右处理到left下标时,左边的最大值left_max对它而言是可信的,但right_max对它而言是不可信的。(见下图,由于中间状况未知,对于left下标而言,right_max未必就是它右边最大的值)
定理三:当我们从右往左处理到right下标时,右边的最大值right_max对它而言是可信的,但left_max对它而言是不可信的。
1 | right_max |
对于位置left
而言,它左边最大值一定是left_max,右边最大值“大于等于”right_max,这时候,如果left_max<right_max
成立,那么它就知道自己能存多少水了。无论右边将来会不会出现更大的right_max,都不影响这个结果。 所以当left_max<right_max
时,我们就希望去处理left下标,反之,我们希望去处理right下标。
1 | int trap(vector<int>& height) { |
排序
来源:https://blog.csdn.net/yushiyi6453/article/details/76407640 (讲的比较好,而且还有动图显示)
注:
1 归并排序可以通过手摇算法将空间复杂度降到O(1),但是时间复杂度会提高。
2 基数排序时间复杂度为O(N*M),其中N为数据个数,M为数据位数。
辅助记忆
时间复杂度记忆
冒泡、选择、直接 排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n2)(一遍找元素O(n),一遍找位置O(n))
快速、归并、希尔、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogn)(一遍找元素O(n),一遍找位置O(logn))
稳定性记忆-“快希选堆”(快牺牲稳定性)
排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。
交换类排序
冒泡排序
每一次把最大的放到后面【确定最大值,放入合适的位置。直至整个数据位置确定完】
1 | //冒泡 |
快速排序
挖坑填数:
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
此时,a[i]左边的值 <a[i]右边的值。再排序左区间与右区间,直至左右区间无值。
1 | // 左闭右闭 |
插入类排序
直接插入排序
分为排序区和未排序区,每次未排序区依次选择一个数,与排序区比较,若排序区的元素比该数大,就插入(元素后移一位,该数放置其前)
1 | //选择排序 |
希尔排序
将数组分组,在各组中进行直接插入排序。逐渐缩小分组间隙,直至整个数组恰被分成一组.
1 | void shellSort(vector<int> &a){ |
选择类排序
简单选择排序
分为排序区和未排序区,每次在未排序区找到最小值,放入排序区的后面。
1 | // 选择排序 |
堆排序
这位大佬图解很清晰,思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素”沉”到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
用数组表示
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
1 | // 堆排序 |
归并类排序
归并排序
采用分治策略(将问题分成一些小的问题,然后递归求解;再将每个阶段的结果合并)
“治”采用双指针的方式合并。
1 | ////在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间 |
桶排
桶排序
划分多个范围相同的区间,每个自区间自排序,最后合并。
1 |
|