十道腾讯算法真题解析!(二)

tony_ybliu
发布于 2022-5-27 17:54
浏览
0收藏

 

5. 最长回文子串 
给你一个字符串 s,找到 s 中最长的回文子串。

实例1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

这道题可以使用中心扩展法实现,从中间开始向两边扩散来判断回文串。

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    更新答案

但是回文串可能是长度可能是奇数,也可能是偶数,因此需要加多一步:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    找到以 s[i] 和s[i+1] 为中心的回文串
    更新答案

完整代码如下:

class Solution {
    public String longestPalindrome(String s) {

        if(s==null|| s.length()<2){
             return s;
        }

        String result ="";
        for(int i=0;i<s.length();i++){
            String r1 = subLongestPalindrome(s,i,i);
            String r2 = subLongestPalindrome(s,i,i+1);
            String tempMax= r1.length()>r2.length()? r1 :r2;
            result = tempMax.length()> result.length()?tempMax:result;
            
        }
        return result;

    }

    private String subLongestPalindrome(String s,int l,int r){
        while(l>=0&&r<s.length()&&s.charAt(l)==s.charAt(r)){
            l--;
            r++;
        }

        return s.substring(l+1,r);
    }
}

 

6.全排列 
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

这道题可以用回溯算法解决,完整代码如下:

class Solution {
    //全排列,即所有路径集合
    List<List<Integer>> allPath = new LinkedList<>();

    public List<List<Integer>> permute(int[] nums) {
        //当前路径,入口路径,path是空的
        List<Integer> path =  new LinkedList<>();
        //递归函数入口,可做选择是nums数组
        backTrace(nums,path);
        return allPath;
    }

    public void backTrace(int[] nums,List<Integer> path){
        //已走路径path的数组长度等于nums的长度,表示走到叶子节点,所以加到全排列集合
        if(nums.length==path.size()){
           allPath.add(new LinkedList(path));
           return;
        }

        for(int i=0;i<nums.length;i++){
            //剪枝,排查已经走过的路径
            if(path.contains(nums[i])){
                continue;
            }
            //做选择,加到当前路径
            path.add(nums[i]);
            //递归,进入下一层的决策
            backTrace(nums,path);
            //取消选择
            path.remove(path.size() - 1);
        }
    }
}

大家可以看下之前我写的这篇回溯文章哈,有回溯算法的框架套用。

面试必备:回溯算法详解

7.  LRU 缓存 
请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过

capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

这道题,出现的频率还是挺高的,很多小伙伴在面试时,都反馈自己遇到过原题。

LRU,Least Recently Used,即最近使用过的数据是有用的,可以使用双链表+Hashmap解答,双链表用于存储LRUCache的数据,Hashmap实现O(1)的平均时间复杂度。

 十道腾讯算法真题解析!(二)-鸿蒙开发者社区

  • 每次从链表尾部添加元素,靠尾的元素就是最近使用过
  • 某个key可以通过哈希表快速定位到节点。
    对于双链表,需要做哪些事呢。
  • 首先是链表初始化,为了方便处理i,虚拟一个头节点和尾结点。
  • 添加元素时,放到链表的尾部,表示该元素最近使用过
  • 删除双向链表的某个节点
  • 删除并返回头节点,表示删除最久未使用的元素
  • 返回链表当前长度

LRU缓存有哪些方法

  • 构造函数初始化方法
  • get和put方法
  • makeRecently 设置某个元素最近使用过的方法,哈希表已经有该元素
  • addRecently 添加最近使用过的元素,同时更新map
  • deleteKey 删除某个key对应的元素,同时删除map上的节点
  • removeLeastRecently  删除最久未使用的元素
    完整代码如下:
class Node {

    int key,val;
    Node next,prev;
    public Node(int key,int val){
        this.key = key;
        this.val = val;
    }
}

class DoubleList {
    //虚拟出头节点和尾结点
    private Node head, tail;
    private int size;

    //初始化双链表
    public DoubleList() {
        //虚拟头结点
        head = new Node(0, 0);
        //虚拟头结点
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    //要加到链表尾部,且越靠近链表尾部,越表示最近使用过
    public void addLast(Node x) {
        //比如当前链表为:head <-> 1 <-> tail,加入结点x = 2
        x.prev = tail.prev;
        // 完成结点2指向两端的箭头  head <-> 1 <- 2 -> tail; 此时tail.pre = 结点1还未断开
        x.next = tail;
        //head <-> 1 <-> 2 -> tail;
        tail.prev.next = x;
        //head <-> 1 <-> 2 <-> tail;
        tail.prev = x;
        //更新链表长度
        size++;
    }

    // 删除指定结点
    public void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }

    // 删除并返回头结点
    public Node removeHead() {
        if (head.next == tail) {
            return null;
        }
        Node first = head.next;
        // size在remove中更新了
        remove(first);
        // 用作在哈希表中移除最久未使用的数据值
        return first;
    }

    // 获取链表长度
    public int getSize() {
        return size;
    }
}

public class LRUCache {

    private Map<Integer, Node> map;
    private DoubleList doubleList;
    private int cap;

    public LRUCache(int capacity) {
        this.map = new HashMap<>();
        this.doubleList = new DoubleList();
        this.cap = capacity;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            // 先将key标记为最近使用,再返回value
            makeRecently(key);
            return map.get(key).val;
        } else {
            return -1;
        }
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            deleteKey(key); // 从原map中移除该key
            addRecently(key, value); // 更新最近使用
            return;
        }

        int size = doubleList.getSize();
        if (size == cap) { // 说明需要移除最久未使用的元素了
            removeLeastRecently();
        }
        addRecently(key, value); //添加新的元素进来
    }


    public void makeRecently(int key) { // 将某个key标记为最近使用的元素(map中已存在的)
        Node x = map.get(key);
        doubleList.remove(x); // 先从双链表删除
        doubleList.addLast(x); // 再添加到链表末尾, 因为尾部是最近使用过的元素
    }

    public void addRecently(int key, int value) { // 添加最近使用过的元素
        Node x = new Node(key, value);
        doubleList.addLast(x);
        map.put(key, x);  //更新map
    }

    public void deleteKey(int key) {
        Node x = map.get(key);
        map.remove(key);
        doubleList.remove(x); // 在map中和cache中同时删除
    }

    // 删除最久未使用的元素
    public void removeLeastRecently() {
        // 最久未使用的一定在链表头部
        Node oldNode = doubleList.removeHead();
        int oldKey = oldNode.key;
        map.remove(oldKey);
    }
}

 

8.  合并K个升序链表 
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

合并两个有序链表,是比较简单的,相信大家都会做。那么如何合并K个有序链表呢?其实道理是一样的,我们可以借用优先级队列找出最小节点,完整代码如下:

class Solution {


    public ListNode mergeKLists(ListNode[] lists) {

        if(lists.length==0){
            return null;
        }
        //虚拟节点
        ListNode head = new ListNode(0);
        ListNode tail = head;
        //优先队列
        PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length,(a, b)->(a.val-b.val));

        //将K个链表头节点合并最小堆
        for (ListNode node: lists) {
            if (node != null) {
                queue.add(node);
            }
        }

        while (!queue.isEmpty()) {
            //获取最小节点,放到结果链表中
            ListNode node = queue.poll();
            tail.next = node;
            
            if (node.next != null) {
                queue.add(node.next);
            }
            //指针链表一直往前
            tail = tail.next;
        }
        return head.next;
    }
}

 

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

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

这道题可以使用滑动窗口来实现。滑动窗口就是维护一个窗口,不断滑动,然后更新答案。

滑动窗口的大致逻辑框架,伪代码如下:

int left =0,right = 0;
while (right < s.size()){
  //增大窗口
  window.add(s[right]);
  right++;
  
  while (window needs shrink){
    //缩小窗口
    window.remove (s[left]);
    left ++;
  }
}

解法流程如下:

  • 首先呢,就是获取原字符串的长度。
  • 接着维护一个窗口(数组、哈希、队列)
  • 窗口一步一步向右扩展
  • 窗口在向右扩展滑动过程,需要判断左边是否需要缩减
  • 最后比较更新答案
    完整代码如下:
int lengthOfLongestSubstring(String s){
     //获取原字符串的长度
     int len = s.length();
     //维护一个哈希集合的窗口
     Set<Character> windows = new HashSet<>();
     int left=0,right =0;
     int res =0;

     while(right<len){
       char c = s.charAt(right);
       //窗口右移
       right++;

       //判断是否左边窗口需要缩减,如果已经包含,那就需要缩减
       while(windows.contains(c)){
          windows.remove(s.charAt(left));
           left++;
       }
       windows.add(c);
       //比较更新答案
       res = Math.max(res,windows.size());
      }
     return res;
}

 

之前写过一篇滑动窗口解析,大家有兴趣可以看下哈:

leetcode必备算法:聊聊滑动窗口

10.删除链表的倒数第 N 个结点 
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

 十道腾讯算法真题解析!(二)-鸿蒙开发者社区
示例 :

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

这道题可以使用双指针解决。既然我们要找到倒数第n个节点,我们可以使用两个指针first 和 second同时对链表进行遍历,并且first 比second超前 n个节点。当 first遍历到链表的末尾时,second 就恰好处于倒数第n个节点。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode first = head;
        ListNode second = dummy;
        //first 比second先走n个节点
        for (int i = 0; i < n; ++i) {
            first = first.next;
        }
        //直到走到链表尾部
        while (first != null) {
            first = first.next;
            second = second.next;
        }
        //删除节点
        second.next = second.next.next;
        ListNode ans = dummy.next;
        return ans;
    }
}

 

参考与感谢 

  • leetcode官网
  • labuladong算法小抄

 

文章转自公众号:小白debug

分类
标签
已于2022-5-27 17:54:43修改
收藏
回复
举报
回复
    相关推荐