面试算法热门考点

154 阅读15分钟

面试算法热门考察

反转链表(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+)

  • 快排+剪枝
  • 注意一些细节
    1. Random函数是nextInt(r-l+1)+l
    1. 每一轮快排中的交换完成后要更新i或j的下标
    1. 注意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;
}
  1. 两两合并,分治合并以及优先队列三者的复杂度分析。
  • 假设链表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)
  1. 分治合并:
  • 第一次合并 k/2组,每一组是2n。
  • 第二次合并 k/4组,每一组是4n
  • 第logk次合并 1组,
  • 每一组是kn
  • 所以总共为 (k/2)*2n+(k/4)*4n+...+kn = logk * kn
  • 空间复杂度为递归用到的o(logk)空间的栈空间
  1. 优先队列
  • 由于优先队列是以堆的形式构成的,并且最多的时候只有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)

  1. 直接模拟
  • 时间复杂度 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();
}
  1. 找规律
  • 每两个位子(分别为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)

  1. 先判断有没有0
  2. 判断两者大小,如果小减大则先置换最后再添加个负号
  3. 相减,每一位(x-y-borrow+10)%10
  4. 计算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方法,降低空间复杂度
  1. 当前节点P,桥节点s
  2. 如果P无左节点,P加入res,P=P.right;
  3. 如果P有左节点,找到桥节点s
  • 如果s有右节点,s加入res,s=s.right
  • 如果s无右节点,s.right = P ,P = P.left
  1. 注意找前置节点的时候小心死循环,多加个!=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];
    
}