栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

算法面试高频系列

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

算法面试高频系列

在面试过程中,一些手撕代码的高频问题值得总结和记录。如:Top K 问题的多种解法、一道结合「简单数据结构 & 简单算法」的面试题、既能考察对「数据结构」的掌握,还能考察对「递归函数」的设计、

文章目录
      • 一、Top K 问题的多种解法
        • 703. 数据流中的第 K 大元素
        • 1. 冒泡排序法
        • 2. 快速排序法
        • 3. 优先队列法
      • 二、一道结合「简单数据结构 & 简单算法」的面试题
        • 21. 合并两个有序链表
        • 1. 双指针解法(哨兵技巧)
        • 2. 递归法实现
      • 三、既能考察对「数据结构」的掌握,还能考察对「递归函数」的设计
        • 24. 两两交换链表中的节点
        • 1. 递归解法
        • 2. 迭代解法(哨兵技巧)

一、Top K 问题的多种解法 703. 数据流中的第 K 大元素

leetcode题目链接:703. 数据流中的第 K 大元素

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

示例一:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素
1. 冒泡排序法

每次调用 add 时先将数装入数组,然后遍历 k 次,通过找 k 次最大值来找到 Top K。

但冒泡排序会导致超时。

class KthLargest {
    // 冒泡排序
    int k;
    List list = new ArrayList<>(10009);

    public KthLargest(int k, int[] nums) {
        this.k = k;
        for (int i : nums) {
            list.add(i);
        }
    }    
    public int add(int val) {  // 冒泡排序
        list.add(val);
        int cur = 0;
        for (int i = 0; i < k; i++) {  // 遍历k次,通过找k次最大值找到Top K
            int idx = findMax(cur, list.size() - 1);
            swap(cur++, idx);
        }
        return list.get(cur - 1);
    }
    int findMax(int start, int end) {
        int ans = 0, max = Integer.MIN_VALUE;
        for (int i = start; i <= end; i++) {
            int t = list.get(i);
            if (t > max) {
                max = t;
                ans = i;
            }
        }
        return ans;
    }
    void swap(int a, int b) {
        int c = list.get(a);
        list.set(a, list.get(b));
        list.set(b, c);
    }
}
2. 快速排序法

上述的解法时间复杂度是 O(nk) 的,当 k 很大的时候会超时。

我们可以使用快排来代替冒泡。

class KthLargest {
    // 快排
    int k;
    List list = new ArrayList<>(10009);

    public KthLargest(int k, int[] nums) {
        this.k = k;
        for (int i : nums) {
            list.add(i);
        }
    }    
    public int add(int val) {
        list.add(val);
        Collections.sort(list);
        return list.get(list.size() - k);
    }
}

Collections.sort 内部最终会调用 Arrays.sort 进行排序。而 Arrays.sort() 本身不只有「双轴快排」一种实现,在排序数量少的情况下会直接使用「冒泡排序」,这里的分析是假定了 Collections.sort 最终使用的是 Arrays.sort 的「双轴快排」。

3. 优先队列法

使用优先队列构建一个容量为 k 的小根堆。

将 nums 中的前 k 项放入优先队列(此时堆顶元素为前 k 项的最大值)。

随后逐项加入优先队列:

  1. 堆内元素个数达到 k 个:
  • 加入项小于等于堆顶元素:加入项排在第 k 大元素的后面。直接忽略
  • 加入项大于堆顶元素:将堆顶元素弹出,加入项加入优先队列,调整堆
  1. 堆内元素个数不足 k 个,将加入项加入优先队列

将堆顶元素进行返回(数据保证返回答案时,堆内必然有 k 个元素)。

class KthLargest {
    // 优先队列
    int k;
    PriorityQueue queue;
    public KthLargest(int k, int[] nums) {
        this.k = k;
        queue = new PriorityQueue<>(k, (a,b)->Integer.compare(a,b));  // 小根堆
        for (int i = 0; i < nums.length && i < k; i++) {
            queue.add(nums[i]);
        }
        for (int i = k; i < nums.length; i++) {
            add(nums[i]);
        }
    }    
    public int add(int val) {
        int t = !queue.isEmpty() ? queue.peek() : Integer.MIN_VALUE;
        if (val > t || queue.size() < k) {  // 加入项大于堆顶元素,将堆顶元素弹出
            if (!queue.isEmpty() && queue.size() >= k) {
                queue.poll();
            }
            queue.add(val);  // 加入项加入优先队列,调整堆
        }
        return queue.peek();  // 堆顶元素就是第K大元素
    }
}

1. 为什么使用小根堆?

  • 因为我们需要在堆中保留数据流中的前 K 大元素,使用小根堆能保证每次调用堆的 pop()函数时,从堆中删除的是堆中的最小的元素(堆顶)。

2. 为什么能保证堆顶元素是第 K 大元素?

  • 因为小根堆中保留的一直是堆中的前 K 大的元素,堆的大小是 K,所以堆顶元素是第 K 大元素。

3. 每次 add() 的时间复杂度是多少?

  • 每次 add() 时,调用了堆的 push() 和 pop() 方法,两个操作的时间复杂度都是 log(K).

参考:

面试题警告:经典 TopK ,本题需要重点学习

【面试高频系列】Top K 问题的多种解法:冒泡排序 & 快速排序 & 优先队列

二、一道结合「简单数据结构 & 简单算法」的面试题 21. 合并两个有序链表

leetcode题目链接:21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
1. 双指针解法(哨兵技巧)

做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。

由于两条链表本身就是有序的,只需要在遍历过程中进行比较即可:

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 1. 双指针,类似于归并排序中的合并过程
        // 虚拟头节点
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                cur.next = list1;
                cur = cur.next;
                list1 = list1.next;
            } else {
                cur.next = list2;
                cur = cur.next;
                list2 = list2.next;
            }
        }
        if (list1 == null) cur.next = list2;
        if (list2 == null) cur.next = list1;
        return dummy.next;
    }
}
2. 递归法实现
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 递归
        if (list1 == null) {
            return list2;
        }
        else if (list2 == null) {
            return list1;
        }
        else if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }
        else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

终止条件:当两个链表都为空时,表示我们对链表已合并完成。

如何递归:我们判断 list1 和 list2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果。(调用递归)

参考:

一看就会,一写就废?详解递归

【面试高频系列】一道结合「简单数据结构 & 简单算法」的面试题

三、既能考察对「数据结构」的掌握,还能考察对「递归函数」的设计 24. 两两交换链表中的节点

leetcode题目链接:24. 两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例一:

输入:head = [1,2,3,4]
输出:[2,1,4,3]
1. 递归解法

链表和树的题目天然适合使用递归来做。

我们可以设计一个递归函数,接受一个 ListNode 节点 root 作为参数,函数的作用是将 root 后面的两个节点进行交换,交换完成后再将下一个节点传入 …

交换的前提条件:节点 root 后面至少有两个节点。

class Solution {
    public ListNode swapPairs(ListNode head) {
        // 递归
        if(head == null || head.next == null) return head;
        ListNode next = head.next;
        ListNode newNode = swapPairs(next.next);
        next.next = head;
        head.next = newNode;
        return next;
    }
}
2. 迭代解法(哨兵技巧)

做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。

class Solution {
    public ListNode swapPairs(ListNode head) {
        // 设置虚拟头节点,交换相邻两个元素
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode tmp = dummy;
        while(tmp.next != null && tmp.next.next != null) {
            ListNode node1 = tmp.next;
            ListNode node2 = tmp.next.next;
            tmp.next = node2;
            node1.next = node2.next;
            node2.next = node1;
            tmp = node1;
        }
        return dummy.next;
    }
}

参考:

【面试高频系列】既能考察对「数据结构」的掌握,还能考察对「递归函数」的设计

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/677488.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号