面试手撕代码(1)

前阵子一直在各种笔试和面试,好在也到手了两个offer,有点懒没怎么刷题,前几个月就把《剑指offer》刷了两遍,leetcode也写了快240道题了,以后刷题会慢慢减少,多写点总结,把企业手撕代码的热题认真过一遍,每篇10题

文章目录

  • 25.k个一组翻转链表——双指针
  • 3.无重复最长子串——hashmap
  • 215. 数组中的第K个最大元素——快排,优先队列
  • 206. 反转链表——递归,迭代
    • 迭代
    • 递归
  • 146. LRU缓存机制——hashmap
  • 160. 相交链表——双指针,浪漫相遇
  • 236. 二叉树的最近公共祖先——递归
  • 15.三数之和——三指针
  • 121. 买股票的最佳时机——dp(重点)
    • 易理解写法
    • dp模板写法
    • dp优化写法
  • 199. 二叉树的右视图——BFS

25.k个一组翻转链表——双指针

两个重点:

  • 反转函数(写烂了)
  • 如何确定需要反转的一串链表
    • 每段链表的前驱指针
    • 每段链表的最后一个节点
public class No25 {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode pre = dummy;//每段链表的前驱指针
        ListNode end = dummy;//每段链表的最后一个节点
        while (end != null) {
            int i = k;
            while (i > 0 && end != null) {
                end = end.next;
                i--;
            }
            //假如最后一组不够k个,就直接不用反转了
            if (end == null) break;
            ListNode start = pre.next;//需要反转的链表的首个节点
            ListNode next = end.next;//下一段需要反转的链表的首个节点

            //下面四句是链表的反转连接
            end.next = null;
            pre.next = reverse(start);
            start.next = next;
            pre = start;
            end = pre;
        }
        return dummy.next;
    }

    public ListNode reverse(ListNode head) {
        ListNode pre = head;
        ListNode cur = null;
        ListNode next = null;
        while (pre != null) {
            next = pre.next;
            pre.next = cur;
            cur = pre;
            pre = next;
        }
        return cur;
    }
}

3.无重复最长子串——hashmap

给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。


public class No3 {
    public int lengthOfLongestSubstring_better(String s) {
        if (s == null || s.length() == 0) return 0;
        HashMap<Character, Integer> map = new HashMap<>();
        int res = 0;
        int left = 0;
        for (int i = 0; i < s.length(); i++) {
            if (map.containsKey(s.charAt(i))) {
                //这一步很秀
                left = Math.max(left, map.get(s.charAt(i)) + 1);
                //考虑abba这种情况,不加left的话指针可能左移
            }
            map.put(s.charAt(i), i);
            res = Math.max(res, i - left + 1);
        }
        return res;
    }
}

215. 数组中的第K个最大元素——快排,优先队列

public class No215 {
    //优先队列,每次弹出小的元素
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0 || nums.length < k) return -1;
        Queue<Integer> queue = new PriorityQueue<>();
        for (int num : nums) {
            queue.offer(num);
            if (queue.size() > k) {
                queue.poll();
            }
        }
        return queue.peek();
    }

    //没必要全部排序,利用快排,找到index=m的元素,m左边都这个元素,
    //m右边都小于这个元素,若k=m+1,返回
    public int findKthLargest(int[] nums, int k) {
        return fastsort(nums, 0, nums.length - 1, k);
    }
    public int fastsort(int[] nums, int lo, int hi, int k) {
        int m = partition(nums, lo, hi, k);
        if (k == m + 1) {
            return nums[m];
        } else if (k < m + 1) {
            return fastsort(nums, lo, m - 1, k);
        } else {
            return fastsort(nums, m + 1, hi, k);
        }
    }
    public int partition(int[] nums, int lo, int hi, int k) {
        int i = lo;
        int j = hi + 1;
        int pivot = nums[lo];
        while (true) {
            while (++i < hi && nums[i] > pivot) ;
            while (--j > lo && nums[j] < pivot) ;
            if (i >= j) {
                break;
            }
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
        nums[lo] = nums[j];
        nums[j] = pivot;
        return j;
    }
}

206. 反转链表——递归,迭代

迭代

public ListNode reverseList_v1(ListNode head) {
    ListNode pre = head;
    ListNode cur = null;
    ListNode temp = null;
    while (pre != null) {
        temp = pre.next;
        pre.next = cur;
        cur = pre;
        pre = temp;
    }
    return cur;
}

递归

  • 递归把 head 结点拿出来,剩下的部分我们调用函数 reverseList ,
  • 这样剩下的部分就逆序了,接着我们把 head 结点放到新链表的尾部就可以了。这就是整个递归的思想了。

public ListNode reverseList(ListNode head) {
    //递归终止条件,一个节点的反转是它本身,如果head值null也直接返回null
    if (head == null || head.next == null) return head;
    ListNode cur = reverseList(head.next);//这里cur就是最后一个节点
    //如果链表是 1->2->3->4->5,那么此时的cur就是5
    //而head是4,head的下一个是5,下下一个是空
    //所以head.next.next 就是5->4
    head.next.next = head;
    head.next = null;//防止链表循环,需要将head.next设置为空
    return cur;//每层递归函数都返回cur,也就是最后一个节点
}

146. LRU缓存机制——hashmap

  • 自己实现双向链表
class DLinkedNode {
    int key;
    int value;
    DLinkedNode prev;
    DLinkedNode next;
    public DLinkedNode() {}
    public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
  • LRU一个映射,通过HashMap的key定位Node
    方便删除和添加,我们初始化一个伪头结点,和一个伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
//存储映射关系key——>node
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
//最大容量
private int capacity;
//伪头结点,伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
private DLinkedNode head, tail;
  • 总体代码
public class No146 {
    //自己实现一个双向链表节点
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    //存储映射关系key——>node
    private int size;
    private int capacity;//最大容量
    private DLinkedNode head, tail;
    //伪头结点,伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
    public No146(int capacity){
        this.size=0;
        this.capacity=capacity;
        head=new DLinkedNode();
        tail=new DLinkedNode();
        head.next=tail;
        tail.prev=head;
    }
    public int get(int key){
        DLinkedNode node=cache.get(key);//先通过哈希表定位
        if(node==null) return -1;//key空,不存在
        moveToHead(node);//如果key存在,再移到头部
        return node.value;
    }
    public void put(int key,int value){
        DLinkedNode node=cache.get(key);
        if(node==null){
            DLinkedNode newnode=new DLinkedNode(key,value);
            cache.put(key,newnode);
            addToHead(newnode);//添加至双向链表的头部
            ++size;
            if(size>capacity){
                DLinkedNode tail=removeTail();//如果超出容量,删除双向链表的尾部节点
                cache.remove(tail.key);//删除哈希表中对应的项
                --size;
            }
        }else{
            node.value=value;//如果key存在,先通过哈希表定位,再修改value,并移到头部
            moveToHead(node);
        }
    }
    public void addToHead(DLinkedNode node){
        DLinkedNode next=head.next;
        node.prev=head;
        node.next=next;
        next.prev=node;
        head.next=node;
    }
    public void removeNode(DLinkedNode node){
        node.prev.next=node.next;
        node.next.prev=node.prev;
    }
    public void moveToHead(DLinkedNode node){
        removeNode(node);
        addToHead(node);
    }
    public DLinkedNode removeTail(){
        DLinkedNode node=tail.prev;
        removeNode(node);
        return node;
    }
}

160. 相交链表——双指针,浪漫相遇

public class No160 {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA != null ? pA.next : headB;
            pB = pB != null ? pB.next : headA;
        }
        //若不相交,相遇处是null(都走了一遍a和b)
        return pA;
    }
}

236. 二叉树的最近公共祖先——递归

class No236{
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //root到了叶子节点,直接返回null
        //root=p或者q的时候直接返回
        if (root == null || root == p || root == q ){
            return root;
        }
        //向左节点和右节点递归
        TreeNode left=lowestCommonAncestor(root.left,p,q);
        TreeNode right=lowestCommonAncestor(root.right,p,q);
        if(left==null){
            return right;
        }
        if(right==null){
            return left;
        }
        return root;
    }
}

15.三数之和——三指针

  • 排序
  • 跳过重复项,减少不必要的判断
  • 第一个指针指向的元素大于0,直接break
public class No15 {
    /**
     * 时间复杂度:O(N^2),外循环kO(N),内循环双指针找和O(N)
     * @param nums
     * @return
     */
    public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0) return res;
        Arrays.sort(nums);
        //固定第一个指针,剩下两个数用左右指针找
        for (int k = 0; k < nums.length - 2; k++) {
            //如果第一数(三者里面最小的)都大于0,那么跳过此次循环
            if (nums[k] > 0) break;
            //跳过重复的,减少不必要的判断
            if (k > 0 && nums[k] == nums[k - 1]) continue;
            //定义双指针
            int i = k + 1;
            int j = nums.length - 1;
            while (i < j) {
                int sum = nums[k] + nums[i] + nums[j];
                if (sum == 0) {
                    res.add(new ArrayList<>(Arrays.asList(nums[k], nums[i], nums[j])));
                    //跳过重复的,减少不必要的判断
                    while (i < j && nums[i] == nums[++i]) ;
                    while (i < j && nums[j] == nums[--j]) ;
                } else if (sum > 0) {
                    while (i < j && nums[j] == nums[--j]) ;
                } else {
                    while (i < j && nums[i] == nums[++i]) ;
                }
            }
        }
        return res;
    }
}

121. 买股票的最佳时机——dp(重点)

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。注意:你不能在买入股票前卖出股票。

易理解写法

public int maxProfit2(int[] prices) {
    int cost = Integer.MAX_VALUE;
    int profit = 0;
    for (int price : prices) {
        cost = Math.min(cost, price);
        profit = Math.max(profit, price - cost);
    }
    return profit;
}

dp模板写法

股票问题其实就是一个三维的dp,哪三维?—— 天数,交易次数,状态
我们用dp[i][k][0 or 1]来表示这个转移状态

dp[i][k][1]:表示第i天还有k次交易次数,手中持有股票的最大利润
dp[i][k][1]:表示第i天还有k次交易次数,手中持没有股票的最大利润

我们就是要求dp[n-1][k][0]

状态转移方程:

dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) ##今天我没有持有股票,有两种可能
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) ##今天我持有股票,有两种可能

base case:

dp[-1][k][0] = 0
因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0
dp[-1][k][1] = -infinity
没开始是不可能持有股票的,⽤负⽆穷表⽰这种不可能
dp[i][0][0] = 0
k 至少是1,k = 0 意味着根本不允许交易,利润当然是 0
dp[i][0][1] = -infinity
不允许交易是不可能持有股票的,⽤负⽆穷表⽰这种不可能

Leetcode121中 k = 1,base case中 dp[i-1][k-1][0] = dp[i-1][0][0] = 0,所以根据状态转移方程 k 只剩下1的情况,可以把k消掉,变成二维dp

public int maxProfit(int[] prices) {
    int n = prices.length;
    int[][] dp = new int[n][2];//[][0]表示没有持有[][1]表示持有
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {//处理base case
            dp[i][0] = 0;
            //dp[i][0] = Math.max(dp[-1][0], dp[-1][1] + prices[0]) 
            //= Math.max(0,-infinity + price) = 0
            dp[i][1] = -prices[i];
            //dp[i][0] = Math.max(dp[-1][1], dp[-1][0] - prices[0])
            //= Math.max(-infinity,0 - price) = -price
            continue;
        }
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
    }
    return dp[n - 1][0];
}

dp优化写法

其实状态转移⽅程中,新状态只和相邻的⼀个状态有关,只需要⼀个变量储存相邻的那个状态就够了,这样可以把空间复杂度降到 O(1)
注意这里的base case和之前的写法不同

public int maxprofit(int[] prices) {
    int n = prices.length;
    // base case: dp[-1][0] = 0, dp[-1][1] = -infinity
    int keep = 0;
    int sell = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        keep = Math.max(keep, sell + prices[i]);
        sell = Math.max(sell, - prices[i]);
    }
    return keep;
}

199. 二叉树的右视图——BFS

判断一下遍历每层的queue是否到了最后一个就行了。。。

public class No199 {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
                if (i == size - 1) {  //将当前层的最后一个节点放入结果列表
                    res.add(node.val);
                }
            }
        }
        return res;
    }
}

你可能感兴趣的:(面试代码)