面试算法热门考察
反转链表(395+)
- 迭代:定义三个节点即可。 时间o(n),空间o(1)
public ListNode reverseList(ListNode head) {
//迭代
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
- 递归:要点在于要思考到终止节点,在这道题中就是递归到最后一个节点先进行反转,然后反向不断往前。类似于二叉树的递归也都是先考虑叶子结点然后不断向上。
- 使尾巴节点一直跟着递归往前走,沿途将节点反转。
public ListNode reverseList(ListNode head) {
//递归
//先考虑最深处(二叉树)或者最后一个节点(链表)
//如果head为最后一个节点或者为null,则并不需要反转,直接返回。
if(head==null||head.next==null) return head;
//这个newHead从递归开始到结束一直都是最后一个节点,跟着递归一直往上传递,最后返回。
ListNode newHead = reverseList(head.next);
//从后往前不断的去反转
head.next.next = head;
//用于开的节点最后能指向null,防止死循环
head.next = null;
return newHead;
}
反转链表2(110+)
- 关键在于头插法和设置一个dummy哑节点,时间为on
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
for(int i=0;i<left-1;i++){
pre = pre.next;
}
ListNode cur = pre.next;
for(int i=0;i<right-left;i++){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
}
K个一组反转链表(185)
- 重点在于哑节点和头插法就可以解决。
- 简单模拟
- 时间为O(n),空间为O(1)
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
int sum = 0;
ListNode pre = head;
while(pre!=null){
sum++;
pre = pre.next;
}
pre = dummy;
ListNode cur = head;
int loopNum = sum / k;
int last = sum % k;
for(int i=0;i<loopNum;i++){
for(int j=0;j<k-1;j++){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
pre = cur;
cur = cur.next;
}
//如果要求不足k个也需要反转
// for(int i=0;i<last-1;i++){
// ListNode next = cur.next;
// cur.next = next.next;
// next.next = pre.next;
// pre.next = next;
// }
return dummy.next;
}
- 递归写法
- 注意递归函数的作用是反转以head开头的k个节点,当调用该方法时,只要知道他已经实现了该功能不需要知道具体细节。
- 递归要先考虑深层或者最后面节点的情况,也就是最后一个节点或者最后一组节点,最后一组节点可能不足k个,则不需要反转直接返回当前的head。
- 则在翻转函数中只需要获取下一组的翻转函数,然后翻转当前组的函数,然后拼接返回新的newHead即可。
public ListNode reverseKGroup(ListNode head, int k) {
//递归写法
ListNode p = head;
int num = 0;
while(num<k){
if(p==null) {
return null;
// 如果不足k个也返回
// return reverse(head,num);
}
p = p.next;
num++;
}
ListNode subListHead = reverseKGroup(p,k);
ListNode newHead = reverse(head,k);
head.next = subListHead;
return newHead;
}
public ListNode reverse(ListNode head,int k){
ListNode pre = null;
ListNode cur = head;
for(int i=0;i<k;i++){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
LRU缓存机制(275+)
- LRU最近最少使用
- 需要自定义一个class节点类,保存有key,value。
- 一个hashmap存储节点key值和对应的节点。
- 需要有一个双端链表(通过节点的next和pre指针来实现)。每次get或者put都需要将节点插入或转移到链表头。
- 当超过指定容量时,将链表尾部的节点去除。
- 时间为o1
class Node{
int key;
int value;
Node next;
Node pre;
public Node(){}
public Node(int key,int value){
this.key = key;
this.value = value;
}
}
class LRUCache {
private Map<Integer,Node> map;
private int capacity;
private Node head;
private Node tail;
public LRUCache(int capacity) {
map = new HashMap<>();
this.capacity = capacity;
head = new Node();
tail = new Node();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
Node node = map.get(key);
if(node!=null){
removeToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
Node node = map.get(key);
if(node!=null){
node.value = value;
removeToHead(node);
}else{
node = new Node(key,value);
addFirst(node);
map.put(key,node);
capacity--;
if(capacity<0){
Node removeNode = remove(tail.pre);
map.remove(removeNode.key);
capacity++;
}
}
}
public Node remove(Node node){
node.pre.next = node.next;
node.next.pre = node.pre;
return node;
}
public void addFirst(Node node){
node.next = head.next;
node.pre = head;
head.next.pre = node;
head.next = node;
}
public void removeToHead(Node node){
remove(node);
addFirst(node);
}
}
-
先给面试官讲一遍LRU和LinkedHashMap的八股,
-
LRU即最近最少使用,是操作系统中常用的页面置换算法,选择最近最久没有使用的页面进行淘汰,在操作系统层面是给每一个页面设置一个时间t,用来记录上一次被访问到现在的时长,每次淘汰t最大的那一个。
-
JAVA中
无重复字符的最长子串(270+)
- 利用滑动窗口,时间on
- 定义一个left来表示无重复的子串起时为止,一开始都是为0。(即在当前i的情况下,加入left前面的元素就会发生重复,此时i-left+1就是无重复的)
- 通过hashmap来存储元素及其下标。
- 每次遍历查看是否存在,如果存在将left更新。
public int lengthOfLongestSubstring(String s) {
if(s.length()<=1) return s.length();
Map<Character,Integer> map = new HashMap<>();
int left = 0;
int max = 0;
for(int i=0;i<s.length();i++){
char c = s.charAt(i);
if(map.containsKey(c)){
left = Math.max(left,map.get(c)+1);
}
map.put(c,i);
max = Math.max(max,i-left+1);
}
return max;
}
最小覆盖子串(60+)
- 滑动窗口
- 先写出大概框架然后用count来简化。
- 一次遍历t字符串,更新need
- 一次遍历s字符串,先通过j去将need减少,直到count为0,然后i
public String minWindow(String s, String t) {
if(s.length()<t.length()) return "";
char[] ss = s.toCharArray();
char[] ts = t.toCharArray();
//大小写存128位
int[] need = new int[128];
//用于判断是否已经覆盖,count表示t中不相同的字符个数
int count = 0;
//第一次遍历t,计算需要的字符个数
for(char c:ts){
if(need[c-'A']++ == 0){
count++;
}
}
int res_i = 0,res_len = Integer.MAX_VALUE;
int i=0,j=0;
while(j<s.length()){
//通过j去更新need和count
while(j<s.length()){
if(--need[ss[j++]-'A'] == 0){
count--;
}
if(count==0) break;
}
//如果count为0说明i-j中已经找到覆盖,这时候要缩小i的范围直到找到最小的范围。并且要使count不为0使得后面的循环继续。
if(count==0){
while(i<j){
if(need[ss[i++]-'A']++ == 0){
count++;
break;
}
}
if(j-i+1<res_len){
res_len = j-i+1;
res_i = i-1;
}
}
}
return res_len==Integer.MAX_VALUE?"":s.substring(res_i,res_i+res_len);
}
长度最小的子数组(30)
- 滑动窗口
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int res = n+1;
int i = 0,j = 0;
int sum = 0;
while(j<n){
while(j<n){
sum+=nums[j++];
if(sum>=target) break;
}
if(sum>=target){
while(i<j){
sum-=nums[i++];
if(sum<target) break;
}
if(j-i+1<res){
res = j-i+1;
}
}
}
return res==n+1?0:res;
}
滑动窗口最大值(60)
- 滑动窗口
- 利用一个单调递减栈来存储最大值的下标。
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] res = new int[n-k+1];
int idx = 0;
//存储下标
Deque<Integer> stack = new ArrayDeque<>();
int i = 0,j = 0;
while(j<k){
while(!stack.isEmpty()&&nums[j]>nums[stack.peekLast()]){
stack.pollLast();
}
stack.offerLast(j);
j++;
}
res[idx++] = nums[stack.peekFirst()];
while(j<n){
if(nums[i]==nums[stack.peekFirst()]){
stack.pollFirst();
}
i++;
while(!stack.isEmpty()&&nums[j]>nums[stack.peekLast()]){
stack.pollLast();
}
stack.offerLast(j);
j++;
res[idx++] = nums[stack.peekFirst()];
}
return res;
}
数组中的第K个最大元素(250+)
- 快排+剪枝
- 注意一些细节
-
- Random函数是nextInt(r-l+1)+l
-
- 每一轮快排中的交换完成后要更新i或j的下标
-
- 注意p和k的比较,因为快排已经是从大到小排序,k小就在左,k大就在右。
- 时间复杂度 on:
public int findKthLargest(int[] nums, int k) {
return quickSort(nums,0,nums.length-1,k-1);
}
public int quickSort(int[] nums,int l,int r,int k){
int p = partition(nums,l,r);
if(p==k) {
return nums[p];
}
return p<k?quickSort(nums,p+1,r,k):quickSort(nums,l,p-1,k);
}
public int partition(int[] nums,int l,int r){
int Rand = new Random().nextInt(r-l+1)+l;
swap(nums,l,Rand);
int t = nums[l];
while(l<r){
while(l<r&&nums[r]<t) r--;
if(l<r){
nums[l] = nums[r];
l++;
}
while(l<r&&nums[l]>t) l++;
if(l<r){
nums[r] = nums[l];
r--;
}
}
nums[l] = t;
return l;
}
public void swap(int[] nums,int l,int r){
int t = nums[l];
nums[l] = nums[r];
nums[r] = t;
}
- 堆排序
public int findKthLargest(int[] nums, int k) {
int n = nums.length;
//构建堆
for(int i = n/2-1;i>=0;i--){
sink(nums,i,n-1);
}
//遍历
for(int i=0;i<k-1;i++){
swap(nums,0,n-i-1);
sink(nums,0,n-i-2);
}
return nums[0];
}
public void sink(int[] nums,int start,int end){
int i = start;
int j = i * 2 + 1;
int t = nums[i];
while(j<=end){
if(j<end&&nums[j+1]>nums[j]){
j++;
}
if(t>nums[j]) break;
nums[i] = nums[j];
i = j;
j = i * 2 + 1;
}
nums[i] = t;
}
public void swap(int[] nums,int i,int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
快速排序
- 时间复杂度:
- 最好情况下:如果每次partition都划分的很平均,则递归树的深度就为logn,o(nlogn)
- T(n) = 2T(n/2) + n
- = 4T(n/4) + 2n
- = 8T(n/8) + 3n
- = nT(1) + nlog2n
- = nlog2n
- 最坏情况下即正序或逆序,递归树为斜着的树,需要调用n-1次递归,第i次递归调用需要比较n-i次,累加后为o(n2)
- 空间复杂度:根据递归树,最好是log2n,最差是n
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort(int[] nums,int l,int r){
if(l<r){
int p = partition(nums,l,r);
quickSort(nums,l,p-1);
quickSort(nums,p+1,r);
}
}
public int partition(int[] nums,int l,int r){
int Random = new Random().nextInt(r-l+1)+l;
swap(nums,r,Random);
int pivot = nums[r];
int p = l;
for(int i = l;i<r;i++){
if(nums[i]<=pivot){
swap(nums,p++,i);
}
}
swap(nums,p,r);
return p;
}
public void swap(int[] nums,int i,int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
堆排序
-
重点在于下沉函数
-
首先利用下沉函数构建堆,调整堆
-
如果需要从小到大建立大根堆,如果要从大到小建立小根堆。
-
堆排序的复杂度计算分为构建堆和调整堆:
-
构建堆:
-
设高度为h,节点为1个时,比较次数为H-1,节点为2个时,比较次数为H-2,......,节点为2的H-1次个时,比较次数为0.
-
s = (H-1)+(2^1)(H-2)+(2^2)(H-3)+...+(2^(H-1))*0
-
2s = 2(H-1)+(2^2)(H-2)+(2^3)(H-3)+...+(2^H)*0
-
两者相减
-
s = 1+2+2^2+2^3+...+2^(H-1)-H = (a1*(1-q^n))/(1-q) = (1-2^H)/(-1)-H = 2^H-1-H
-
令H为logn s = n-1-logn 所以复杂度为o(n)
-
调整堆,
-
需要进行n-1次调整,一次调整的时间复杂度为o(logn),所有总的时间复杂度为o(nlogn)
-
空间复杂度因为是原地排序所以是o1
public int[] sortArray(int[] nums) {
int n = nums.length;
// 构建大顶堆
for(int i =n/2-1;i>=0;i--){
sink(nums,i,n-1);
}
//遍历
for(int i=0;i<n;i++){
swap(nums,0,n-i-1);
sink(nums,0,n-i-2);
}
return nums;
}
public void sink(int[] nums,int start,int end){
int i = start;
int j = start * 2 + 1;
int t = nums[i];
while(j<=end){
if(j<end&&nums[j]<nums[j+1]){
j++;
}
if(t>nums[j]) break;
nums[i] = nums[j];
i = j;
j = i * 2 + 1;
}
nums[i] = t;
}
public void swap(int[] nums,int i,int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
归并排序
- 重点在于需要一个额外的temp数组来存储中间值
- 合并的时候先按从小到大放入temp数组,最后将temp数组覆盖到num数组。
- 时间复杂度T(n) = 2T(n/2) + O(n) = O(nlogn)
- 空间复杂度一个temp数组和递归调用所需要logn的栈空间,总共为O(n).
int[] temp;
public int[] sortArray(int[] nums) {
//归并排序
int n = nums.length;
temp = new int[n];
mergeSort(nums,0,n-1);
return nums;
}
public void mergeSort(int[] nums,int l,int r){
if(l>=r) return ;
int m = (l+r)/2;
//分
mergeSort(nums,l,m);
mergeSort(nums,m+1,r);
//合
int idx = 0;
int i = l,j = m+1;
while(i<=m&&j<=r){
if(nums[i]<=nums[j]){
temp[idx++] = nums[i++];
}else{
temp[idx++] = nums[j++];
}
}
while(i<=m){
temp[idx++] = nums[i++];
}
while(j<=r){
temp[idx++] = nums[j++];
}
for(i=0;i<idx;i++){
nums[l+i] = temp[i];
}
}
三数之和(170+)
- 排序,首尾双指针,两次去重
- 然后要跳过一些重复数字
- z用于最外层循环找到第一个数字x,ij通过双指针的方式找到-x。
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
if(n<=2) return res;
//排序
Arrays.sort(nums);
for(int z=0;z<n-2;z++){
//第一次去重复
if(z>0&&nums[z]==nums[z-1]) continue;
int target = -nums[z];
//首尾双指针
int i=z+1,j=n-1;
while(i<j){
int k = nums[i] + nums[j];
if(k==target){
res.add(Arrays.asList(nums[z],nums[i],nums[j]));
//第二次去重
i++;
j--;
while(i<j&&nums[i]==nums[i-1]) i++;
while(i<j&&nums[j]==nums[j+1]) j--;
}else if(k<target){
i++;
}else j--;
}
}
return res;
}
两数之和(155+)
- 利用map存储下标
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
if(map.containsKey(target-nums[i])){
return new int[]{i,map.get(target-nums[i])};
}
map.put(nums[i],i);
}
return new int[0];
}
最大子序和(160)
- 动态规划
- 判断前一个是否为正数(为正数代表对最大值有意义),是就加上,不是就从自己开始算起(贪心的思想)
- 因为每一次dp的计算都只与前一位相关,可以去掉数组用两个变量代替。只存储当前位和前一位。
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int max = Integer.MIN_VALUE;
for(int i=0;i<n;i++){
if(i>0&&dp[i-1]>0){
dp[i] = dp[i-1] + nums[i];
}else dp[i] = nums[i];
max = Math.max(max,dp[i]);
}
return max;
}
- 用变量代替
public int maxSubArray(int[] nums) {
int n = nums.length;
int a = 0,b;
int max = Integer.MIN_VALUE;
for(int i=0;i<n;i++){
if(a>0) b = a + nums[i];
else b = nums[i];
a = b;
max = Math.max(max,b);
}
return max;
}
- 扩展题:要求返回所有的子数组
- 记录start和len
public int maxSubArray(int[] nums) {
int n = nums.length;
int a = nums[0],b;
int max = Integer.MIN_VALUE;
int start = 0,len = 1;
int max_start = 0,max_len = 1;
for(int i=1;i<n;i++){
if(a>0) {
b = a + nums[i];
len++;
}else {
b = nums[i];
start = i;
len = 1;
}
a = b;
if(b>max){
max_start = start;
max_len = len;
max = b;
}
}
for(int i=0;i<max_len;i++){
System.out.print(nums[max_start+i]);
}
return max;
}
环形链表(155)
- 判断是否有环
- 快慢指针
public boolean hasCycle(ListNode head) {
if(head==null) return false;
ListNode slow = head;
ListNode fast = head;
while(fast!=null&&fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast==slow) return true;
}
return false;
}
环形链表2(100+)
- 找环的入口,设环外节点个数为a,环内节点个数为b,则环的入口为a+kb
- 当快慢指针相遇时,f走过s的两倍,换个说法f多走了kb个节点
- 即f=2s,f=s+kb,可得出s=kb
- 即快慢指针相遇时已经走了kb个节点了,这时候离结果只剩下a,则将快指针置换到head,快指针走到环的入口正好是a步,而慢指针走到环的入口也正好是a步,所以两者再次相遇就是走了a步,这就找出了a+kb了。
public ListNode detectCycle(ListNode head) {
if(head==null) return null;
ListNode slow = head;
ListNode fast = head;
while(true){
if(fast==null||fast.next==null) return null;
slow = slow.next;
fast = fast.next.next;
if(slow==fast){
break;
}
}
fast = head;
while(slow!=fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
扩展:计算环中节点的个数
- 计算环中节点的个数即计算b的大小
- 因为f和s都需要跑a的距离,而f为了套圈要多跑b的距离,f和s每跑一次他们之间的距离就会缩短1,s跑了b步时f和s会正好相遇。
- 所以s = b只需要计算第一次相遇时s走过的路程。
合并两个有序链表(150+)
- 迭代写法
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
p.next = l1;
l1 = l1.next;
}else{
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
p.next = l1==null?l2:l1;
return dummy.next;
}
- 递归写法
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}else if(l2==null){
return l1;
}else if(l1.val<=l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
合并两个有序数组
- 为了不使用额外的空间,采用倒序。
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m-1,j = n-1;
int t = m+n-1;
while(i>=0||j>=0){
int x1 = i>=0?nums1[i]:Integer.MIN_VALUE;
int x2 = j>=0?nums2[j]:Integer.MIN_VALUE;
if(x1>=x2){
nums1[t--] = x1;
i--;
}else{
nums1[t--] = x2;
j--;
}
}
}
合并k个排序链表
-
复杂度
-
除了归并还有其他方法吗
-
分治法
public ListNode mergeKLists(ListNode[] lists) {
return mergeSort(lists,0,lists.length-1);
}
public ListNode mergeSort(ListNode[] list,int left,int right){
if(left>right) return null;
if(left==right) return list[left];
int m = (left + right) >> 1;
ListNode leftNode = mergeSort(list,left,m);
ListNode rightNode = mergeSort(list,m+1,right);
return merge(leftNode,rightNode);
}
public ListNode merge(ListNode l1,ListNode l2){
if(l1==null){
return l2;
}else if(l2==null){
return l1;
}else if(l1.val<=l2.val){
l1.next = merge(l1.next,l2);
return l1;
}else{
l2.next = merge(l1,l2.next);
return l2;
}
}
- 优先队列
public ListNode mergeKLists(ListNode[] lists) {
// 优先队列的实现方法
if(lists==null) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>((l1,l2)->{
return l1.val - l2.val;
});
for(ListNode list:lists){
if(list!=null){
queue.offer(list);
}
}
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while(!queue.isEmpty()){
ListNode node = queue.poll();
if(node.next!=null){
queue.offer(node.next);
}
p.next = node;
p = p.next;
}
return dummy.next;
}
- 两两合并,分治合并以及优先队列三者的复杂度分析。
- 假设链表a和b的长度都是n,总共有k个链表。
- 则合并两个链表的时间复杂度是O(2n)
- 两两合并:第一次合并为O(n),第二次合并为O(2n),第i次合并为O(in),所以总和的为O((1+2+3+...+k)n) = o(k^2*n)
- 空间复杂度为o(1)
- 分治合并:
- 第一次合并 k/2组,每一组是2n。
- 第二次合并 k/4组,每一组是4n
- 第logk次合并 1组,
- 每一组是kn
- 所以总共为 (k/2)*2n+(k/4)*4n+...+kn = logk * kn
- 空间复杂度为递归用到的o(logk)空间的栈空间
- 优先队列
- 由于优先队列是以堆的形式构成的,并且最多的时候只有k个元素,每一次插入删除都是logk,总共有kn个元素,所以时间复杂度为logk * kn
- 空间复杂度为k个空间的优先队列。
合并区间
- 按照第一位先排序
- 然后依次遍历,如果当前的第一位比上一个的第二位还要大,则直接插入数组,反之则和上一个合并。
public int[][] merge(int[][] intervals) {
Deque<int[]> res = new ArrayDeque<>();
Arrays.sort(intervals,(p1,p2)->{
return p1[0]-p2[0];
});
for(int[] interval:intervals){
if(res.isEmpty()||res.peekLast()[1]<interval[0]){
res.offerLast(interval);
}else{
int t[] = res.pollLast();
t[1] = Math.max(t[1],interval[1]);
res.offerLast(t);
}
}
return res.toArray(new int[res.size()][2]);
}
- 时间复杂度:排序用到nlogn,遍历n,转换为数组为n,最后总的为nlog
- 空间复杂度:存储结果为n,排序用到logn。
二叉树的层序遍历(150)
- 迭代方式
- 复杂度为on,空间复杂度为on
public List<List<Integer>> levelOrder(TreeNode root) {
//迭代
List<List<Integer>> res = new ArrayList<>();
if(root==null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int k = queue.size();
List<Integer> temp = new ArrayList<>();
for(int i=0;i<k;i++){
TreeNode node = queue.poll();
temp.add(node.val);
if(node.left!=null) queue.offer(node.left);
if(node.right!=null) queue.offer(node.right);
}
res.add(temp);
}
return res;
}
- 递归版本
- 时间复杂度为on,空间复杂度为oh,h为树的高度
List<List<Integer>> res;
public List<List<Integer>> levelOrder(TreeNode root) {
//递归
res = new ArrayList<>();
order(root,0);
return res;
}
public void order(TreeNode root,int level){
if(root==null) return ;
if(res.size()<=level){
res.add(new ArrayList<>());
}
res.get(level).add(root.val);
order(root.left,level+1);
order(root.right,level+1);
}
二叉树的锯齿形层次(135+)
- 迭代
- 注意用位运算来判断奇偶性
- num & 1 == 0?偶数:奇数;
- 当res.size为奇数时采用头插法插入temp。
- LinkedList才有addFirst和addLast两个方法。
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root==null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int k = queue.size();
LinkedList<Integer> temp = new LinkedList<>();
for(int i=0;i<k;i++){
TreeNode node = queue.poll();
if(node.left!=null) queue.offer(node.left);
if(node.right!=null) queue.offer(node.right);
if((res.size()&1)==1){
temp.addFirst(node.val);
}else{
temp.addLast(node.val);
}
}
res.add(temp);
}
return res;
}
- 递归方法
List<List<Integer>> res ;
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
res = new ArrayList<>();
order(root,0);
return res;
}
public void order(TreeNode root,int level){
if(root==null) return ;
if(res.size()<=level){
res.add(new ArrayList<>());
}
if((level&1)==1){
res.get(level).add(0,root.val);
}else{
res.get(level).add(root.val);
}
order(root.left,level+1);
order(root.right,level+1);
}
- ArrayList get 头插法add(0,num) 尾插法add(num)
- LinkedList get addFirst addLast
二叉树的层次遍历2
- 从叶子结点到根结点
- 递归方法
List<List<Integer>> res ;
public List<List<Integer>> levelOrderBottom(TreeNode root) {
res = new ArrayList<>();
order(root,0);
return res;
}
public void order(TreeNode root,int level){
if(root==null) return ;
if(res.size()<=level){
res.add(0,new ArrayList<>());
}
res.get(res.size()-level-1).add(root.val);
order(root.left,level+1);
order(root.right,level+1);
}
买卖股票的最佳时机(140+)(还需总结)
- 维护前面的最小值即可
public int maxProfit(int[] prices) {
int min = prices[0];
int res = 0;
for(int i=1;i<prices.length;i++){
min = Math.min(min,prices[i]);
res = Math.max(res,prices[i]-min);
}
return res;
}
买卖股票的最佳时机2(30+)
- 贪心算法,赚取所有的利润
public int maxProfit(int[] prices) {
int res = 0;
for(int i=1;i<prices.length;i++){
if(prices[i]>prices[i-1]){
res+=prices[i]-prices[i-1];
}
}
return res;
}
买卖股票的最佳时机3(17)
- 定义状态然后进行状态转移
- 动态规划
- dp[天数][是否持有股票][卖出的次数] 表示在第i天结束时,最多进行k次交易的情况下可以获得的最大收益。
- 由于当前天数的dp只和昨天的dp有关,可以去掉天数这一维度
买卖股票的最佳时机4(8)
相交链表(140)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pa = headA;
ListNode pb = headB;
while(pa!=pb){
pa = pa==null?headB:pa.next;
pb = pb==null?headA:pb.next;
}
return pa;
}
二叉树的最近公共祖先(125+)
TreeNode res = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
judge(root,p,q);
return res;
}
public boolean judge(TreeNode root,TreeNode p,TreeNode q){
if(root==null) return false;
boolean inCur = root.val==p.val||root.val==q.val;
boolean inleft = judge(root.left,p,q);
boolean inright = judge(root.right,p,q);
if((inCur&&(inleft||inright))||(inleft&&inright)){
res = root;
return true;
}
return inCur||inleft||inright;
}
有效的括号(125)
- 利用栈去处理
public boolean isValid(String s) {
if((s.length()&1)==1) return false;
Deque<Character> stack = new ArrayDeque<>();
for(char c:s.toCharArray()){
if(c=='{'){
stack.offerLast('}');
}else if(c=='['){
stack.offerLast(']');
}else if(c=='('){
stack.offerLast(')');
}else if(stack.isEmpty()||stack.pollLast()!=c){
return false;
}
}
return stack.isEmpty();
}
- 按照规定的括号顺序进行开闭如何实现?
- 将优先级存放在map中
public boolean isValid(String s) {
if((s.length()&1)==1) return false;
Map<Character,Integer> map = new HashMap<>(){{
put(')',0);
put('}',1);
put(']',2);
put('(',0);
put('{',1);
put('[',2);
}};
Deque<Character> stack = new ArrayDeque<>();
int cur = 2;
for(char c:s.toCharArray()){
if(c=='{'){
if(stack.isEmpty()){
cur = map.get(c);
}
stack.offerLast('}');
}else if(c=='['){
if(stack.isEmpty()){
cur = map.get(c);
}
stack.offerLast(']');
}else if(c=='('){
if(stack.isEmpty()){
cur = map.get(c);
}
stack.offerLast(')');
}else if(stack.isEmpty()||stack.pollLast()!=c||map.get(c)>cur){
return false;
}
}
return stack.isEmpty();
}
字符串相加(120)
- 倒着加就完事了
- 每个字符转换为数字进行相加加到stringbuilder上,最后反转再toString
public String addStrings(String num1, String num2) {
int l1 = num1.length()-1;
int l2 = num2.length()-1;
StringBuilder sb = new StringBuilder();
int res = 0;
while(l1>=0||l2>=0||res!=0){
if(l1>=0) res+=(num1.charAt(l1--)-'0');
if(l2>=0) res+=(num2.charAt(l2--)-'0');
sb.append(res%10);
res /= 10;
}
return sb.reverse().toString();
}
字符串相乘(47)
- 直接模拟
- 时间复杂度 n为num1,m为num2,相乘的次数为mn次,相加的次数为n次,每次的最大长度为n+m,所以相加的时间的复杂度为n2+nm。
- 空间复杂度为中间字符串的长度,最大为n+m
public String multiply(String num1, String num2) {
if("0".equals(num1)||"0".equals(num2)) return "0";
// 模拟笔算的步骤
int l1 = num1.length()-1,l2 = num2.length()-1;
String res = "";
for(int i=l1;i>=0;i--){
StringBuilder temp = new StringBuilder();
for(int z=i;z<l1;z++){
temp.append("0");
}
int sum = 0;
for(int j=l2;j>=0;j--){
sum += (num1.charAt(i)-'0') * (num2.charAt(j)-'0');
temp.append(sum%10);
sum/=10;
}
if(sum>0){
temp.append(sum);
}
res = add(res,temp.reverse().toString());
}
return res;
}
public String add(String num1,String num2){
StringBuilder res = new StringBuilder();
int l1 = num1.length() - 1,l2 = num2.length() - 1;
int sum = 0;
while(l1>=0||l2>=0||sum>0){
if(l1>=0) sum+=num1.charAt(l1--)-'0';
if(l2>=0) sum+=num2.charAt(l2--)-'0';
res.append(sum%10);
sum/=10;
}
return res.reverse().toString();
}
- 找规律
- 每两个位子(分别为i和j)的数相乘都直接与最后的结果数组的i+j位和i+j+1位相关。
- sum = res[i+j+1] + num1[i] * num2[j]
- res[i+j+1] = sum % 10;
- res[i+j] += sum/10;
- 复杂度为nm,空间为n+m
public String multiply(String num1, String num2) {
// 找规律
if("0".equals(num1)||"0".equals(num2)) return "0";
int l1 = num1.length(),l2 = num2.length();
int res[] = new int[l1+l2];
for(int i = l1-1;i>=0;i--){
int n = num1.charAt(i) - '0';
for(int j=l2-1;j>=0;j--){
int m = num2.charAt(j) -'0';
int sum = res[i+j+1] + n * m;
res[i+j+1] = sum % 10;
res[i+j] += sum / 10;
}
}
StringBuilder sb = new StringBuilder();
for(int z=0;z<l1+l2;z++){
if(z==0&&res[z]==0) continue;
sb.append(res[z]);
}
return sb.toString();
}
字符串相减(7)
- 先判断有没有0
- 判断两者大小,如果小减大则先置换最后再添加个负号
- 相减,每一位(x-y-borrow+10)%10
- 计算borrow,就判断x-y-borrow是不是>=0;
public String addStrings(String num1, String num2) {
//字符串相减
if("0".equals(num1)) return "-"+num2;
if("0".equals(num2)) return num1;
boolean sub = false;
//比较两者大小
if(flag(num1,num2)==true){
String t = num1;
num1 = num2;
num2 = t;
sub = true;
}
//相减
StringBuilder sb = new StringBuilder();
int l1 = num1.length()-1,l2 = num2.length()-1;
int borrow = 0;
while(l1>=0||l2>=0){
int x=0,y=0;
if(l1>=0) x=num1.charAt(l1--)-'0';
if(l2>=0) y=num2.charAt(l2--)-'0';
sb.append((x-y-borrow+10)%10);
borrow = x-y-borrow>=0?0:1;
}
if(sub==true){
return "-"+sb.reverse().toString();
}
return sb.reverse().toString();
}
boolean flag(String num1,String num2){
if(num1.length()==num2.length()){
for(int i=0;i<num1.length();i++){
if(num1.charAt(i)==num2.charAt(i)) continue;
return num1.charAt(i)<num2.charAt(i);
}
}
return num1.length()<num2.length();
}
最长回文子串(113)
- 由中间向两边扩展。
- 时间复杂度n2,空间复杂度1
int idx = 0;
int len = 1;
public String longestPalindrome(String s) {
char[] cs = s.toCharArray();
for(int i=0;i<cs.length-1;i++){
palindrome(cs,i,i);
palindrome(cs,i,i+1);
}
return s.substring(idx,idx+len);
}
public void palindrome(char cs[],int i,int j){
while(i>=0&&j<cs.length&&cs[i]==cs[j]){
i--;
j++;
}
if(j-i-1>len){
len = j-i-1;
idx = i+1;
}
}
- 动态规划 定义dp[i][j]为s[i...j]是否为回文子串 时间复杂度为n2 空间为n2
public String longestPalindrome(String s) {
int n = s.length();
if(n<2) return s;
int idx = 0;
int len = 1;
boolean[][] dp = new boolean[n][n];
for(int i=0;i<n;i++){
dp[i][i] = true;
}
for(int l=2;l<=n;l++){
for(int i=0;i<n;i++){
int j = i+l-1;
if(j>=n) break;
if(s.charAt(i)!=s.charAt(j)) {
dp[i][j] = false;
continue;
}
if(j-i+1<=3){
dp[i][j] = true;
}else {
dp[i][j] = dp[i+1][j-1];
}
if(dp[i][j]&&j-i+1>len){
len = j-i+1;
idx = i;
}
}
}
return s.substring(idx,idx+len);
}
岛屿数量
- dfs
- 时间mn,空间mn最差的时候深度搜索全部,递归栈为mn
public int numIslands(char[][] grid) {
int res = 0;
boolean[][] used = new boolean[grid.length][grid[0].length];
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]=='1'&&!used[i][j]){
dfs(grid,i,j,used);
res++;
}
}
}
return res;
}
public void dfs(char[][] grid,int x,int y,boolean[][] used){
if(x<0||x>=grid.length||y<0||y>=grid[0].length||used[x][y]||grid[x][y]=='0') return;
used[x][y] = true;
dfs(grid,x+1,y,used);
dfs(grid,x-1,y,used);
dfs(grid,x,y+1,used);
dfs(grid,x,y-1,used);
}
- bfs:本质上就是队列
- 时间是mn,空间是min(m,n),广度遍历碰到边缘就结束了。
public int numIslands(char[][] grid) {
int n = grid.length;
int m = grid[0].length;
boolean used[][] = new boolean[n][m];
Queue<int[]> queue = new LinkedList<>();
int res = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]=='1'&&!used[i][j]){
queue.offer(new int[]{i,j});
used[i][j] = true;
res++;
}
while(!queue.isEmpty()){
int[] node = queue.poll();
int x = node[0];
int y = node[1];
if(x-1>=0&&grid[x-1][y]=='1'&&!used[x-1][y]){
queue.offer(new int[]{x-1,y});
used[x-1][y] = true;
}
if(x+1<grid.length&&grid[x+1][y]=='1'&&!used[x+1][y]){
queue.offer(new int[]{x+1,y});
used[x+1][y] = true;
}
if(y-1>=0&&grid[x][y-1]=='1'&&!used[x][y-1]){
queue.offer(new int[]{x,y-1});
used[x][y-1] = true;
}
if(y+1<grid[0].length&&grid[x][y+1]=='1'&&!used[x][y+1]){
queue.offer(new int[]{x,y+1});
used[x][y+1] = true;
}
}
}
}
return res;
}
- 并查集
- 时间mn*(find中路径压缩和union按秩合并,通常为o(1))的操作
- 空间mn
int count;
int parent[];
int rank[];
public UnionFind(char[][] grid){
count = 0;
int n = grid.length;
int m = grid[0].length;
parent = new int[n * m];
rank = new int[n * m];
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]=='1'){
count++;
parent[i * m + j] = i * m + j;
}
}
}
}
public int find(int x){
if(x!=parent[x]) parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x1,int x2){
int p1 = find(x1);
int p2 = find(x2);
if(p1!=p2){
if(rank[p1]<rank[p2]){
parent[p1] = p2;
}else if(rank[p1]>rank[p2]){
parent[p2] = p1;
}else{
parent[p1] = p2;
rank[p2] ++;
}
count--;
}
}
public int getCount(){
return count;
}
}
class Solution {
public int numIslands(char[][] grid) {
//并查集
UnionFind Union = new UnionFind(grid);
int n = grid.length;
int m = grid[0].length;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]=='1'){
grid[i][j] = '0';
if(i-1>=0&&grid[i-1][j]=='1'){
Union.union(i * m + j,(i-1) * m + j);
}
if(i+1<n&&grid[i+1][j]=='1'){
Union.union(i * m + j,(i+1) * m + j);
}
if(j-1>=0&&grid[i][j-1]=='1'){
Union.union(i * m + j,i * m + (j-1));
}
if(j+1<m&&grid[i][j+1]=='1'){
Union.union(i * m + j,i * m + (j+1));
}
}
}
}
return Union.getCount();
}
全排列
- dfs
- 时间n个数字,每个数字都有n!个字节点,所有总共时间复杂度为n*n!,空间n
List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) {
res = new ArrayList<>();
boolean used[] = new boolean[nums.length];
dfs(nums,new ArrayList<>(),used);
return res;
}
public void dfs(int[] nums,List<Integer> path,boolean[] used){
if(path.size()==nums.length){
res.add(new ArrayList(path));
return ;
}
for(int i=0;i<nums.length;i++){
if(used[i]) continue;
used[i] = true;
path.add(nums[i]);
dfs(nums,path,used);
path.remove(path.size()-1);
used[i] = false;
}
}
搜索旋转排序数组
- 二分搜索
- 一分为二,先判断一边是不是有序,然后判断这个目标值在不在这个范围内,然后不断缩小范围。
- 时间logn,空间1
public int search(int[] nums, int target) {
int n = nums.length;
int l = 0,r = n-1;
while(l<=r){
int m = (l+r)/2;
if(nums[m]==target) return m;
if(nums[0]<=nums[m]){
if(nums[0]<=target&&target<nums[m]){
r = m-1;
}else{
l = m+1;
}
}else{
if(nums[m]<target&&target<=nums[r]){
l = m+1;
}else{
r = m-1;
}
}
}
return -1;
}
螺旋矩阵(101)
- 模拟
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
int n = matrix.length;
int m = matrix[0].length;
int l=0,r=m-1,u=0,b=n-1;
while(l<=r&&u<=b){
for(int i=l;i<=r;i++){
res.add(matrix[u][i]);
}
u++;
if(l>r||u>b) break;
for(int i=u;i<=b;i++){
res.add(matrix[i][r]);
}
r--;
if(l>r||u>b) break;
for(int i=r;i>=l;i--){
res.add(matrix[b][i]);
}
b--;
if(l>r||u>b) break;
for(int i=b;i>=u;i--){
res.add(matrix[i][l]);
}
l++;
if(l>r||u>b) break;
}
return res;
}
二分查找
- 迭代和递归,整型溢出,效率
public int search(int[] nums, int target) {
// 迭代
int l = 0,r = nums.length-1;
while(l<=r){
// 先减后加防止整形溢出,位运算提高效率
int m = ((r-l)>>1)+l;
if(nums[m]==target){
return m;
}else if(nums[m]<target){
l = m+1;
}else{
r = m-1;
}
}
return -1;
}
int res = -1;
public int search(int[] nums, int target) {
// 递归
search(nums,0,nums.length-1,target);
return res;
}
public void search(int[] nums,int l,int r,int target){
if(l>r) return ;
int m = ((r-l)>>1)+l;
if(nums[m]==target){
res = m;
return ;
}else if(nums[m]>target){
search(nums,l,m-1,target);
}else{
search(nums,m+1,r,target);
}
}
接雨水(93)
- 动态规划双遍历,算出左右两边最高的立柱,然后取其中最小的减去当前高度就是可以蓄水的,然后不断累加。
- 时间n,空间n
public int trap(int[] height) {
int n = height.length;
int[] left = new int[n];
int[] right = new int[n];
left[0] = height[0];
for(int i=1;i<n;i++){
left[i] = Math.max(left[i-1],height[i]);
}
right[n-1] = height[n-1];
for(int i=n-2;i>=0;i--){
right[i] = Math.max(right[i+1],height[i]);
}
int res = 0;
for(int i=1;i<n-1;i++){
res += Math.min(left[i],right[i])-height[i];
}
return res;
}
- 双指针来优化空间复杂度,并且只用一次遍历
- 思路:比较双指针的大小,当left小于right时,right就没有意义了,因为当right不断靠近left的时候,right是不断变大的,而我们计算时是取其中小的,所以left肯定是小于(当前的right和不断靠近的right),然后再比较left和left_max,如果left比leftmax还要大,说明存不住水,不加入res但是要更新leftmax,如果left比leftmax小,则计算res。
public int trap(int[] height) {
int n = height.length;
int l = 0,r = n-1;
int lm = 0,rm = 0;
int res = 0;
while(l<r){
if(height[l]<height[r]){
if(height[l]<lm){
res+=lm-height[l];
}else{
lm = height[l];
}
l++;
}else{
if(height[r]<rm){
res+=rm-height[r];
}else{
rm = height[r];
}
r--;
}
}
return res;
}
二叉树中序遍历
- 迭代
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
while(!stack.isEmpty()||p!=null){
while(p!=null){
stack.offerLast(p);
p = p.left;
}
if(!stack.isEmpty()){
p = stack.pollLast();
res.add(p.val);
p = p.right;
}
}
return res;
}
- Morris方法,降低空间复杂度
- 当前节点P,桥节点s
- 如果P无左节点,P加入res,P=P.right;
- 如果P有左节点,找到桥节点s
- 如果s有右节点,s加入res,s=s.right
- 如果s无右节点,s.right = P ,P = P.left
- 注意找前置节点的时候小心死循环,多加个!=p的条件
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode p = root;
while(p!=null){
if(p.left==null){
res.add(p.val);
p = p.right;
}else{
TreeNode s = p.left;
while(s.right!=null&&s.right!=p){
s = s.right;
}
if(s.right==null){
s.right = p;
p = p.left;
}else{
res.add(p.val);
p = p.right;
}
}
}
return res;
}
二叉树的前序遍历(70)
- 迭代
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root==null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
while(p!=null||!stack.isEmpty()){
while(p!=null){
res.add(p.val);
stack.offerLast(p);
p = p.left;
}
if(!stack.isEmpty()){
p = stack.pollLast();
p = p.right;
}
}
return res;
}
- 递归
List<Integer> res;
public List<Integer> preorderTraversal(TreeNode root) {
res = new ArrayList<>();
order(root);
return res;
}
public void order(TreeNode root){
if(root==null) return ;
res.add(root.val);
order(root.left);
order(root.right);
}
最长递增子序列(92)
- 动态规划,记录以当前位置结尾的最长严格递增子序列的长度,每一次往前找所有比他小的dp+1,取其中最大的作为当前。
- 时间复杂度为n2,空间复杂度为n
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int res = 0;
for(int i=1;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]>=nums[i]) continue;
dp[i] = Math.max(dp[i],dp[j]+1);
}
res = Math.max(res,dp[i]);
}
return res+1;
}
- 边找边构建递增数组用i,j定界,如果当前数大于j,放到j后面,如果比j小,二分查找替换大于等于它的数里最小那个数。
- 数组的长度就是最后的结果。dp数组即路径
- 时间复杂度nlogn,空间n
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int size = 0;
for(int i=1;i<n;i++){
if(nums[i]>dp[size]){
dp[++size] = nums[i];
}else {
int l = 0,r = size;
while(l<r){
int m = ((r-l)>>1) + l;
if(nums[i]<=dp[m]){
r = m;
}else{
l = m+1;
}
}
dp[l] = nums[i];
}
}
return size+1;
}
用栈实现队列(90)
- push就入栈1
- pop如果栈2为空,则将栈1全部入栈2,再取栈2,如果不为空,直接取。
- 增加一个front,减少peek的时间复杂度
Deque<Integer> s1;
Deque<Integer> s2;
int front;
public MyQueue() {
s1 = new ArrayDeque<>();
s2 = new ArrayDeque<>();
}
public void push(int x) {
if(s1.isEmpty()){
front = x;
}
s1.push(x);
}
public int pop() {
if(s2.isEmpty()){
while(!s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.pop();
}
public int peek() {
if(!s2.isEmpty()){
return s2.peek();
}
return front;
}
public boolean empty() {
return s1.isEmpty()&&s2.isEmpty();
}
重排链表(85)
public void reorderList(ListNode head) {
// 中点 + 反转 + 合并
ListNode mid = findMid(head);
ListNode l1 = head;
ListNode l2 = mid.next;
mid.next = null;
l2 = reverse(l2);
head = merge(l1,l2);
}
// 找到中点
public ListNode findMid(ListNode head){
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
while(fast!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 反转
public ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
ListNode next = null;
while(cur!=null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
// 合并
public ListNode merge(ListNode l1,ListNode l2){
ListNode head = l1;
while(l1!=null&&l2!=null){
ListNode t1 = l1.next;
ListNode t2 = l2.next;
l1.next = l2;
l1 = t1;
l2.next = l1;
l2 = t2;
}
return head;
}
二叉树的右视图
- dfs递归遍历,同一层越右边的值越迟被遍历到,可以替换res里的。
List<Integer> res ;
public List<Integer> rightSideView(TreeNode root) {
res = new ArrayList<>();
if(root==null) return res;
dfs(root,1);
return res;
}
public void dfs(TreeNode root,int height){
if(root==null) return ;
if(height>res.size()){
res.add(root.val);
}else{
res.set(height-1,root.val);
}
dfs(root.left,height+1);
dfs(root.right,height+1);
}
爬楼梯(83)
- dp 然后空间优化
public int climbStairs(int n) {
if(n<=2) return n;
int a = 1;
int b = 2;
for(int i=3;i<=n;i++){
int c = a + b;
a = b;
b = c;
}
return b;
}
不能爬到7及7的倍数
public int climbStairs(int n) {
if(n<=2) return n;
int a = 1;
int b = 2;
for(int i=3;i<=n;i++){
if(i%7==0) {
a = b;
b = 0;
}else{
int c = a + b;
a = b;
b = c;
}
}
return b;
}
矩阵快速幂(优化时间复杂度)
链表中倒数第k个节点
- 双指针,一个先走k步,然后一起走到底,后走的就到了倒数第k个节点。
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
for(int i=0;i<k;i++){
fast = fast.next;
}
while(fast!=null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
删除链表中的倒数第N个节点(73)
- 注意点,要先找到倒数第n+1个节点。
- 注意最后返回的是dummy.next而不是head,因为head也有可能被删除。
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null) return null;
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
for(int i=0;i<n+1;i++){
fast = fast.next;
}
while(fast!=null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
}
删除排序链表中的重复元素2(71)
- 迭代一次遍历
- 如果cur和cur.next相同,则循环cur直到cur和cur.next不同,此时pre.next就置换为cur.next,这样相同区域就被跳过去了。
- 反之,pre=cur
- 时间复杂度n,空间复杂度1
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while(cur!=null&&cur.next!=null){
if(cur.val==cur.next.val){
while(cur.next!=null&&cur.val==cur.next.val){
cur = cur.next;
}
pre.next = cur.next;
}else{
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
- 递归方法
- 递归函数的意义在于返回已经删除了重复元素的头节点,
- 如果当前head.val=head.next.val则循环找到不相等的那个节点,直接return deleteDuplicates(head.next),就跳过当前和head全部相等的节点,返回的是哪个节点就由递归函数来返回。
- 如果不相等,则要保留当前的head,head.next = deleteDuplicates(head.next).然后向前返回head。
public ListNode deleteDuplicates(ListNode head) {
// 递归写法
if(head==null||head.next==null){
return head;
}
if(head.val==head.next.val){
while(head.next!=null&&head.val==head.next.val){
head = head.next;
}
return deleteDuplicates(head.next);
}else{
head.next = deleteDuplicates(head.next);
}
return head;
}
二叉树中的最大路径和(76)
- dfs,深度优先(本质上也是动态规划的思想),最大路径和为左边子树的最大路径和加上右边子树的最大路径和(前提两个都是大于0)加上本身节点。然后向上层返回左右两个节点中较大的那一个加上本身节点。
int res ;
public int maxPathSum(TreeNode root) {
res = Integer.MIN_VALUE;
dfs(root);
return res;
}
public int dfs(TreeNode root){
if(root==null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
if(left<0) left = 0;
if(right<0) right = 0;
res = Math.max(res,root.val+left+right);
return root.val + Math.max(left,right);
}
如何输出路径
int res ;
// 用于存放最大的输出路径。
List<Integer> maxPath;
// 存放每个节点的最大输出路径。
Map<TreeNode,List<Integer>> map;
public int maxPathSum(TreeNode root) {
res = Integer.MIN_VALUE;
// 选用ArrayList可以使用addALL方法简化代码。
maxPath = new ArrayList<>();
// 必须提前插入一条数据来表明里面的List是ArrayList类型。
map = new HashMap<>(){{
put(null,new ArrayList<>());
}
};
dfs(root);
for(int path:maxPath){
System.out.print(path);
}
System.out.println();
return res;
}
public int dfs(TreeNode root){
if(root==null) return 0;
int left = Math.max(dfs(root.left),0);
int right = Math.max(dfs(root.right),0);
List<Integer> leftList = map.get(root.left);
List<Integer> rightList = map.get(root.right);
// 更新最大输出路径
if(root.val + left + right > res) {
res = root.val + left + right;
maxPath.clear();
if(left>0){
maxPath.addAll(leftList);
}
maxPath.add(root.val);
if(right>0){
maxPath.addAll(rightList);
}
}
// 向上返回时取较长的最大路径拼接上当前节点,存入map中。
List<Integer> temp = new ArrayList<>();
temp.add(root.val);
if(left>right){
temp.addAll(leftList);
map.put(root,temp);
}else{
temp.addAll(rightList);
map.put(root,temp);
}
return root.val + Math.max(left,right);
}
字符串转换整数(72)
- 一顿模拟
- 注意一开始的空格符号和正号负号都在一开始处理掉,后续在循环中遇到的这些符号都当作其他符号来处理。
public int myAtoi(String s) {
if(s.length()==0) return 0;
int n = s.length();
char[] cs = s.toCharArray();
int flag = 1;
int i = 0;
int res = 0;
// 先处理前置的空格和第一个出现的正号或负号
while(i<n&&cs[i]==' ') i++;
if (i == n) {
return 0;
}
if(cs[i]=='-'){
flag = -1;
i++;
}else if(cs[i]=='+'){
i++;
}
while(i<n){
if(cs[i]>='0'&&cs[i]<='9'){
int k = cs[i] - '0';
if(res>Integer.MAX_VALUE/10||(res==Integer.MAX_VALUE/10&&k>Integer.MAX_VALUE%10)){
return Integer.MAX_VALUE;
}
else if(res<Integer.MIN_VALUE/10||(res==Integer.MIN_VALUE/10&&k>-(Integer.MIN_VALUE%10))){
return Integer.MIN_VALUE;
}
res = res * 10 + k * flag;
}else{
break;
}
i++;
}
return res;
}
链表的两数相加(69)
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
int t = 0;
while(l1!=null||l2!=null||t!=0){
if(l1!=null) {
t+=l1.val;
l1 = l1.next;
}
if(l2!=null) {
t+=l2.val;
l2 = l2.next;
}
p.next = new ListNode(t%10);
p = p.next;
t/=10;
}
return dummy.next;
}
排序链表(66)
-
时间复杂度要求nlogn,空间复杂度要求1
-
归并排序,快慢指针找到中心节点,拆分成两个链表,不断拆分,最后合并。
-
归并递归方法(自顶向下)空间复杂度不符合。
public ListNode sortList(ListNode head) {
return mergeSort(head);
}
public ListNode mergeSort(ListNode head){
if(head==null||head.next==null) return head;
ListNode slow = head;
ListNode fast = head.next;
while(fast!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow.next;
slow.next = null;
ListNode left = mergeSort(head);
ListNode right = mergeSort(mid);
return merge(left,right);
}
public ListNode merge(ListNode l1,ListNode l2){
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
p.next = l1;
l1 = l1.next;
}else{
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
p.next = l1==null?l2:l1;
return dummy.next;
}
-
归并排序迭代方法(因为复杂度要求o1,所以不能使用递归)
-
自底向上。
-
用dummy定义整个链表的哨兵,pre定义每次循环子链表的头节点,cur为当前节点
-
通过循环合并链表的长度从1,2,4,。。。,len。
-
注意每次拆了之后要把尾巴节点置为null,不然会空循环。
public ListNode sortList(ListNode head) {
if(head==null||head.next==null) return head;
// 计算链表长度
int len = 0;
ListNode cur = head;
while(cur!=null){
len++;
cur = cur.next;
}
// 定义整个链表的哑节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 循环
for(int k = 1;k < len;k <<= 1){
ListNode pre = dummy;
cur = pre.next;
while(cur!=null){
// 找到两个头节点
ListNode head1 = cur;
for(int i=1;i<k&&cur.next!=null;i++){
cur = cur.next;
}
ListNode head2 = cur.next;
cur.next = null;
cur = head2;
for(int i=1;i<k&&cur!=null&&cur.next!=null;i++){
cur = cur.next;
}
// 保留next指针
ListNode next = null;
if(cur!=null){
next = cur.next;
cur.next = null;
}
// 合并两个头节点
ListNode merge = merge(head1,head2);
pre.next = merge;
// 将pre移动到合并后链表的最后一个位置
while(pre.next!=null){
pre = pre.next;
}
cur = next;
}
}
return dummy.next;
}
- 快速排序
编辑距离(66)
- 动态规划,定义dp[i][j]为word1中0-i位的字符串转换到word2中0-j位的字符串所需要的最少次数
- 如果i为0或者j为0则dp[i][j] = i||j
- 如果word1[i]==word2[j],则dp[i][j] = dp[i-1][j-1],只需要比较前i-1位和j-1位。
- 如果不相等。则需要转换,
- 如果是替换,则是dp[i-1][j-1]+1,
- 如果是增加,则是dp[i][j-1]+1,
- 如果是删除,则是dp[i-1][j]+1,
- 取三者中的最小值。
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
if(n==0||m==0){
return n+m;
}
int[][] dp = new int[n+1][m+1];
for(int i=0;i<=n;i++){
dp[i][0] = i;
}
for(int i=0;i<=m;i++){
dp[0][i] = i;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1])) + 1;
}
}
}
return dp[n][m];
}