54. 螺旋矩阵
1 | private List<Integer> result; |
498. 对角线遍历

1 | /** |
1 | private List<Integer> result; |

1 | /** |
1 | //使用index来代表栈顶位置,index=0,栈为空,index = size,栈满 |
1 | public class MyCircularQueue { |
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) – 将元素 x 推入栈中。
pop() – 删除栈顶的元素。
top() – 获取栈顶元素。
getMin() – 检索栈中的最小元素。
1 | Stack<Integer> data = new Stack<>(); |
队列是先进先出的,我们准备两个队列data,helper,data存放入队的数据,pop的时候将前n-1个元素从data出队到helper中,这样data中就只剩下一个最先进队的元素,即可以达到先进后出的目的。
1 | private Queue<Integer> data = new LinkedList<>(); |
因为栈是先进后出,因此元素先进去一个栈push,在从该栈push出栈到另一个栈pop,这样,我们从pop中取出元素就可以达到先进先出的目的。
需要注意的是,元素从push到pop,需要满足两个条件:需要一次性将push栈元素出栈完;当pop中有元素时,不可以入栈。
1 | Stack<Integer> sta_pop = new Stack<>(); |
BFS:需要记录两层的节点,空间复杂度是指数级别的。O(2^h),queue
DFS:空间复杂度为树的深度。O(h),stack
BFS空间复杂度比较高,但是可以搜索最小性质。
| 空间复杂度 | 数据结构 | ||
|---|---|---|---|
| DFS | O(h),h为树的高度 | stack | |
| BFS | O(2^h),指数级别的 | queue | 可以求出最短路的问题(边的权重是1) |

1 | public int minDepth(TreeNode root) { |
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
a搜索二叉树:左子树小于根节点,右子树大于根节点。
平衡树:左右子树高度差不大于1
一般解决二叉树的问题采用递归以及递归转非递归的方法。
1 | /* |
还有一种做法:中序遍历结果是递增的,即为二叉搜索树
1 | public boolean isValidBST(TreeNode root) { |
1 | public List<Integer> preorderTraversal(TreeNode root) { |
1.将整棵树的左子树压入栈中
2.每次取出栈顶元素,如果他有右子树,则将右子树压入栈中。
1 | /** |
1 | /** |
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1 | 1 |
递归做法
1 | /** |
非递归方法
1 | /** |
递归的方法
1 | public TreeNode buildTree(int[] preorder, int[] midorder) { |
1 | public TreeNode buildTree(int[] inorder, int[] postorder) { |
1 | public TreeNode constructFromPrePost(int[] pre, int[] post) { |
通过队列来解决
1 | public List<List<Integer>> levelOrder(TreeNode root) { |
1 | /** |
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
示例 :
给定二叉树
1 | 1 |
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
1 | /** |
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例 2:
输入: [-10,9,20,null,null,15,7]
-10
/
9 20
/
15 7
输出: 42
1 | int res=Integer.MIN_VALUE; |
可以用中序遍历来解决,但是要求空间复杂度为O(h),h为树的深度。
1 | class BSTIterator { |

1 | /** |
1 | public String serialize(TreeNode root) { |
层次遍历
1 | public String serialize(TreeNode root) { |
1 | public boolean isBalanced(TreeNode root) { |
1 | /** |
1 | /** |
单调栈主要解决以下几个问题:
寻找比当前元素更大的下一个元素或者下一个元素,那么应该使用栈底到栈顶由大到小的单调栈。当某个元素大于栈顶元素,将栈顶元素弹出,而该元素就是栈顶元素的最右侧的第一个大于它的元素。最左侧的最大元素,那么,某个元素在栈中的下一个元素就是大于它的第一个元素。
寻找比当前元素更小的下一个元素或者前一个元素,则使用栈底到栈顶由小到大的单调栈。
给定两个没有重复元素的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出-1。
1 | /** |
1 | /**给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素), |
1 | /** |
下一个更大元素问题,单调递增栈
1 | /** |
1 | /** |
1 | //本质就是找到某个元素左侧第一个小于它的元素,以及右侧小于它的第一个元素 |
本题与上一题类似
1 | /** |
1 | //使用单调递减栈,假如某个元素大于栈顶元素,因为元素栈底到栈顶是由大到小的,所以必定可以形成雨水区域 |
1 | /** |

1 | /** |
关于处理最小字串,最长字串等问题,可以使用滑动窗口来解决。
1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
2、我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
1 | string s, t; |
1 | /** |
1 | public int lengthOfLongestSubstring(String s) { |
多级索引+链表。空间复杂度O(n),查找以及插入时间复杂度O(logn)。
对于链表的查找,常见的形式是O(n)的复杂度,需要遍历链表。

因此,可以抽取索引来解决。这样的话,时间复杂度就可以缩短一半。
skiplist正是受这种多层链表的想法的启发而设计出来的。
他会为每一个节点随机生成一个层数。

查找时间复杂度是O(logn)。
从最高层开始,一层层遍历直到原始链表。

最高层索引:从head开始,head下一个元素未7,所以后移。7下一个未null,所以位于7,null之间,下沉。
二层索引:从7开始,7下一个元素是37,23位于7,37之间。下沉。
一级索引:从7开始,7下一个元素19,23不位于,向右移动,19下一个元素37,23位于。下沉。
原始链表:从19开始,19下一个元素是22,23不位于,向右移动,22下一个元素26,找到23位置。
当插入数据的时候,根据该点生成的层数level,从level层开始,逐层开始插入。跟查找整个过程相似,只不过添加了一个插入元素的过程。
插入数据6,level=3。
三级索引,1<6<13,所以往下移动。
二级索引,6位于1,7之间,所以,直接在1,7之间插入7。
一级索引,6不在1,4之间,所以向后移动,6位于4,7之间,所以插入。
原始链表,6不位于4,5之间,向后移动,6位于5,7之间,直接插入。
与查找,插入数据类似。时间复杂度也是O(logn)。
LRU:当容量满的时候,删除最久未使用的元素。
LFU:当容量满的时候,删除使用次数最少的,假如使用次数最少有多个元素,那么删除最久未使用的元素。
146.设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
1 | /** |
当容量满的时候,删除的是访问次数最少的,假如有多个访问次数最少的节点,删除最久未访问的。
多个桶组成,每个桶代表的是访问次数的数值,桶内存储的是相同访问次数的所有节点,采用头插法,所以尾节点是该桶内最久未访问的节点。
各个桶之间也是使用双向链表来进行存储。
因此,需要定义一个Node:key,value,times(访问次数),pre,next.
两个map,一个存储key与Node,一个存储Node属于哪一个桶。
插入数据:假如map中包含该元素,那么更新value,访问次数++,并将该元素移动到访问次数+1桶。假如没有该元素,直接将该元素插入到访问次数为1的桶。假如超过了size,那么删除第一个桶的尾节点。
get数据:访问次数++,移动到访问次数+1的桶。
检查两个元素是否属于一个集合。
合并两个元素各自所在的所有集合。
并查集需要一次性将所有的数据传给他.
1 | public class UnionFind<K> { |
经常用于统计和排序大量的字符串(但不仅限于字符串)。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
前缀匹配
trie树前缀匹配常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能
字符串检索
给定一组字符串,查找某个字符串是否出现过。
1 | public class Trie { |
| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|---|
| 选择排序 | × | N^2 | 1 | |
| 冒泡排序 | √ | N^2 | 1 | |
| 插入排序 | √ | N ~ N^2 | 1 | 时间复杂度和初始顺序有关 |
| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 |
| 快速排序 | × | NlogN | logN | |
| 三向切分快速排序 | × | N ~ NlogN | logN | 适用于有大量重复主键 |
| 归并排序 | √ | NlogN | N | |
| 堆排序 | × | NlogN | 1 | 无法利用局部性原理 |
快排的最坏时间复杂度,选的初始值是最大值或者是最小值,这样就是N-1+N-2+….1=O(n^2).
最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况,就是二叉树的层数
最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况
1 | /* |
改进版的快速排序,分为<num等于num以及大于num三个子区间,这样的话当数据中有大量相同的数据,就可以大大节省时间。
1 | public static void quick_sort2(int[] nums,int left,int right){ |
1 | /** |
合二为一。将两个排序好的数组进行合并,使用双指针。
空间复杂度:O(N),即tmp数组
时间复杂度:O(nlogN),可有公式求出
1 | /** |
主要借助归并排序来解决。类似于可以划分为左右子区间的问题都可以使用递归方式来解决。

1 | public static int merge(int[] arr,int left ,int right){ |
时间复杂度为O(n^2)。n-1+n-2+….2=n^2/2
从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。
每一次循环都会有一个位置的元素确定。
1 | public static void bubbleSort(int[] arr){ |
与冒泡排序差不多,与冒泡排序最大的区别就就是它的交换元素每一次只交换一次,选择排序是不稳定的。
1 | public static void selectSort(int[] arr){ |
时间复杂度最差是O(n^2),逆序的情况下,最好是O(n),顺序的情况下。
1 | public void insertSort(int[] arr){ |
堆的插入删除时间复杂度是logN,即树的高度,因此,效率很高。
堆实际上是一个完全二叉树,堆可以用数组来表示,位置 k 的节点的父节点位置为 (k-1)/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
大顶堆:父节点的值大于他所有子孙的值。
小顶堆:父节点的值小于他所有子孙的值。
1 | //上浮操作 |
桶排序,基数排序,计数排序。

采用桶的思想来解决。
1 | /** |

例如:
1 | /** |
该递归程序每次需要递归两次子区间,则a = 2,每个递归子区间大小是N/2,因此b = 2,而每次执行完还需要做O(1)的操作,因此,d= 0,log(b,a)=1>d,所以复杂度为O(N^log(2,2))=O(N)
工程中的排序算法
一般来说,是综合多种排序算法的。
假如数据量很小,使用插入排序;样本量很大,数据类型是基础类型,使用快排,数据类型是类,使用归并排序,可以保证数据稳定性。
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
1 | /** |
1 | /* |
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
1 | public ListNode rotateRight(ListNode head, int k) { |
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

1 | //需要交换a,b两个节点时 |
1 | public ListNode swapPairs(ListNode head) { |
1 | public ListNode reverseList(ListNode head) { |


1 | public ListNode reverseBetween(ListNode head, int m, int n) { |

编写一个程序,找到两个单链表相交的起始节点。
求相交的点,两个指针,第一个指针从a走,走a+c,到达尾部,再从b走;
第二个指针从b走,走b+c,到达尾部,再从a走。
这样,两个指针都走了a+b+c,相交点即为所求。
1 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
1 | /** |
空间复杂度为O(1)的方法。
1 | /** |
1 | public ListNode partition(ListNode head, int x) { |
通过哈希表来解决,空间复杂度O(n)
1 | public Node copyRandomList(Node head) { |
空间复杂度为O(1)
1 | /**遍历链表,建立一个包含原节点以及复制节点1->1'->2->2'->3->3' |
1 | /** |
假如需要操纵节点的前置节点,那么在头节点前面添加一个哑节点。
1 | //跳出while循环,此时cur指向的是最后一个节点 |
一个数据库事务通常包含对数据库进行读或写的一个操作序列。
事务有四大特性:原子性,一致性,持久性,隔离性。
事务就是由一跳或者多条SQL语句组成的,事务中的操作要么不做,要么全做。
事务的所有操作要么全部执行成功,要么全部失败回滚。要做就全做,不然不做。
回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
事务应确保数据库的状态从一个一致状态转变为另一个一致状态。在事务开始之前以及事务结束之后,数据库的完整性约束没有被破坏。
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
使用重做日志来保证持久性。
主要分为扁平事务,带保存点的扁平事务,链事务,嵌套事务,分布式事务。
begin work 开始,commit work或者rollback work结束。
要么都执行,要么从头开始回滚。
他的限制是无法回滚或者提交数据的一部分,一旦回滚就得回滚所有。
回滚操作可以选择回滚到某一个保存点。rollback work: 2,回滚到第二个保存点。
多个事务并发执行,经常会发生多个事务操作相同的数据,这就会导致一定的一致性问题。
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。T1再读取的时候就不是自己修改的数据了,而是T2修改之后的。
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读的重点是修改,幻读的重点在于新增或者删除。
通过事务隔离可以解决上面的问题。
最低的隔离级别,即使没有提交,其他事务对于该事务操作也是可见的。
可能会导致脏读、幻读或不可重复读。
均可以使用二分查找。

红色表示满足某一个性质,绿色表示不满足某一个性质。
求红色的右边界情况。
if mid in 红 [L,R]->[M,R] L=M
else mid in 绿 [L,R]->[L,M-1] R=M-1
注意,mid应该是取(L+R)/2+1,防止死循环
1 | while(l<r){ |
求绿色的左边界情况
if mid in 绿 [L,R]->[L,M] R=M
else mid in 红 [L,R]->[M+1,R] L=M+1
mid取值是(L+R)/2
1 | while(l<r){ |
确定二分边界:left = 0,right=x
编写二分代码框架
设定一个性质
判断区间如何更新
如果更新方式是l=mid,r=mid-1,那么mid上取整
按照框架来做:
设定一个性质:t*t<=x
区间如何更新:满足区间的右边界点
1 |
|
边界判断
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
1 | public int searchInsert(int[] nums, int target) { |
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置.
1 | public int[] searchRange(int[] nums, int target) { |
1 | //check:nums[mid]<=target 左侧边界值 |
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。
1 | /** |
搜索一个数。
1 | public int binarySearch(int [] arr,int val){ |
1 | //check:letters[mid]<=target 右侧边界 |
1 | /** |
1 | /** |
shuangzhi双指针可以将时间复杂度从O(n*n)到o(n)
而在双指针的概念中,我们可以将双指针分为两种类型:快慢指针、相向指针,同向指针。
左右指针中间夹,快慢指针走到头,后序指针往回走.