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

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

 

前言 
大家好,我是捡田螺的小男孩。收集了腾讯常考的十道算法题(真题)。在金三银四,希望对大家有帮助呀。

  1. 重排链表
  2. 最长递增子序列
  3. 环形链表
  4. 反转链表
  5. 最长回文子串
  6. 全排列
  7. LRU 缓存
  8. 合并K个升序链表
  9. 无重复字符的最长子串
  10. 删除链表的倒数第 N 个结点
    1.  重排链表
    给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

输入:

head = [1,2,3,4]

输出:

[1,4,2,3]

思路:

如果是数组就好了,哈哈,因为数组可以直接通过下标访问,很容易就可以解答这道题了。但是这是链表。链表不支持下标访问,我们没办法随机访问到链表任意位置的元素,怎么办呢?

我们可以先遍历一下,用数组把链表的元素按顺序存储起来呀,然后就可以把它当做数组这么访问来用了对吧,最后重建下链表即可啦。

ArrayList的底层就是数组,我们先用它存储链表就好,如下:

 List<ListNode> list = new ArrayList<ListNode>();
 ListNode node = head;
 while (node != null) {
    list.add(node);
    node = node.next;
}

有了一个数组结构的链表后,如何重建链表呢?回头多看示例两眼,很容易就发小规律啦:先排第1个,再排倒数第1个,接着排第2个,紧接着倒数第2个。显然这个规律很明显,代码也比较好实现:

int i = 0;
int j = list.size()-1;
while(i<j){
    list.get(i).next = list.get(j);
    i++;
    if(i==j){
      break;
   }
   list.get(j).next = list.get(i);
   j--;
}
//大家画个图就很清晰知道为什么需要这行了,哈哈
 list.get(i).next = null;

完整实现代码如下:

class Solution {
    public void reorderList(ListNode head) {
        if (head == null) {
            return;
        }
        List<ListNode> list = new ArrayList<ListNode>();
        ListNode node = head;
        while (node != null) {
            list.add(node);
            node = node.next;
        }
        int i = 0, j = list.size() - 1;
        while (i < j) {
            list.get(i).next = list.get(j);
            i++;
            if (i == j) {
                break;
            }
            list.get(j).next = list.get(i);
            j--;
        }
        list.get(i).next = null;
    }
}

 

2. 最长递增子序列 
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

实例1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

实例2:

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

思路:

这道题是求最值问题,可以使用动态规划解决。动态规划的解题整体思路就是:

  1. 穷举分析
  2. 分析找规律,拆分子问题
  3. 确定边界
  4. 确定最优子结构
  5. 写出状态转移方程
    2.1 穷举分析
    动态规划的核心思想包括拆分子问题,记住过往,减少重复计算。所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?

自底向上的穷举过程:

  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.
  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。
  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。
  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。
  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。
  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。
  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。
  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。
  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.
    2.2 分析找规律,拆分子问题
    通过上面分析,我们可以发现一个规律:

如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。

原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列
是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:

 十道腾讯算法真题解析!(一)-鸿蒙开发者社区
nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:

 十道腾讯算法真题解析!(一)-鸿蒙开发者社区
其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦

  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]比5小,所以就是[2]+[5]啦,即dp[4]=2
  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]比3小,所以就是[2]+[3]啦,即dp[4]=2
  • nums[5]=7,以7结尾的最长子序列就是[2,5,7]和[2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1。
    很显然有这个规律:一个以nums[i]结尾的数组nums
  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有:dp(i) =max(dp(j))+1。
    2.3 确定边界
    当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1, 因此边界就是dp(1)=1。

2.4 确定最优子结构
从2.2 穷举分析找规律,我们可以得出,以下的最优结构:

dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。

2.5 写出状态转移方程
通过前面分析,我们就可以得出状态转移方程啦:

 十道腾讯算法真题解析!(一)-鸿蒙开发者社区
所以数组nums[i]的最长递增子序列就是:

最长递增子序列 =max(dp[i])

完整代码实现如下:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        //初始化就是边界情况
        dp[0] = 1;
        int maxans = 1;
        //自底向上遍历
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            //从下标0到i遍历
            for (int j = 0; j < i; j++) {
                //找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
                if (nums[j] < nums[i]) {
                    //因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            //求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }
}

 

3. 环形链表 
给定一个链表的头节点head ,返回链表开始入环的第一个节点。如果链表无环,则返回 null。

实例:

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

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

如果判断链表是否有环,我们可以使用快慢指针,快指针是慢指针速度的两倍,当两个指针相遇时,即表示有环。

boolean hasCycle(ListNode head ){

    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;
}

 

我们可以很容易就判断有环,但是如何返回入环的第一个节点呢?我们来画个图分析一波:

 十道腾讯算法真题解析!(一)-鸿蒙开发者社区
假设起点为A,入环点为B,快慢指针相遇点为C,慢指针走到相遇点为k步,B到C的距离为m。设环型周长为X。因为快指针速度是慢指针的2倍。则有:

K-m + X + m = 2K = 快指针走的举例

所以周长X = K。相遇后,快指针到继续往前走,走到入环点B,刚好距离是X-m = K-m。而起点到B节点,距离也是K-m。因此,快慢指针相遇后,慢指针回到起点,这时候快慢指针一样的速度走,相遇时,就是入环点啦,是不是无巧不成书呀,哈哈哈。

完整代码如下:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head ==null){
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        while(fast!=null&&fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;
            //快慢指针相等表示有环
            if(slow==fast){
               //回到起点一起相同速度走
               while(head!=fast){
                   head = head.next;
                   fast = fast.next;
               }
               return head;
            }

        }
        return null;
    }
}

 

4. 反转链表 
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

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

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

完整代码如下:

class Solution {
    public ListNode reverseList(ListNode head) {

        ListNode prev = null;
        ListNode next = head;
        ListNode curr = head ;

        while(curr!=null){
            next =  curr.next ;
            curr. next = prev;
            prev = curr ;
            curr = next ;
        }

        return prev;
    }
}

之前图解过这道题,大家可以看下哈:

看一遍就理解,图解单链表反转

 

文章转自公众号:小白debug

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