7分钟带你细致解析4个Java算法必刷题

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

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

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

**进阶:**你可以想出一个时间复杂度小于 O(n^2) 的算法吗?

解题

1.双层循环(时间复杂度 O(n^2)

var twoSum = function (nums, target) {
    let res = []
    for (let i = 0; i < nums.length - 1; i++) {
        for (let j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] === target) {
                res[0] = i;
                res[1] = j;
                return res;
            }
        }
    }
};

提交截图

 

 

2.使用对象来装进行判断(时间复杂度 O(n)

var twoSum = function (nums, target) {
  let hash = {};
  for (let i = 0; i < nums.length; i++) {
    //判断目标值减去数组里面对应的值如果在对象里面存在的话 返回对应的下标
    if (hash[target - nums[i]] !== undefined) {
      return [i, hash[target - nums[i]]];
    }
    //将数组里面的值当做对象的key 将对应的下标当作值
    hash[nums[i]] = i;
  }
  //未找到返回空数组
  return [];
};

提交截图

 

 

2.两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示:

  • 每个链表中的节点数在范围 [1, 100] 内
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

解题(链表)

/**
 * 定义的链表  val表示值 next表示下一个
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
   //声明一个链表节点 c表示余数默认为0
   let list=new ListNode(),c=0;
   //声明当前的节点
   let cur=list, all;
   //循环判断对应的节点是否为0
   while(l1||l2||c!=0){
       //将当前节点 指向一个新的节点
       cur.next=new ListNode();
       //再将新节点得到(一步步走到底)
       cur=cur.next;
       //得到对应的指向value值
       all=0+c;
       //判断l1的值不为0
       if(l1){
           //将对应节点的值加上之前节点的值
            all+=parseInt(l1.val);
           //继续往下走
            l1=l1.next;
       }
       //判断l2的值不为0
       if(l2){
           //将对应的值加上之前的节点的值
            all+=parseInt(l2.val);
           //继续往下走
            l2=l2.next;
       }
       //将当前的节点的值存入
       cur.val=all%10;
       //得到对应的余数
       c=parseInt(all/10);
      
   }
   //返回list的下一个节点(默认第一个节点为空)
   return list.next;
};

提交截图

 

 

3.无重复字符的最长子串

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

示例 1:

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

示例 2:

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

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

解题

1.暴力破解(以每个字符串为开头遍历一遍,也就是总共遍历两遍。O(n^2)

//暴力解法+Set
var lengthOfLongestSubstring = function(s) {
    //记录长度
    let count=0;
    for(let i=0;i<s.length;i++){
        //实例化set完成去重操作
        let set=new Set();
        //长度默认为0
        let max=0;
        //第二层遍历
        let j=i;
        //如果set不包含对应的元素以及遍历的下标小于长度进入
        while(j<s.length&&!set.has(s[j])){
            //将内容添加到set
            set.add(s[j]);
            //长度++
            max++;
            //控制往后走
            j++;
        }
        //比对得到最大值
        count=Math.max(count,max);
    }
    return count;
};

提交截图

 

 

2.贪心算法(将不重复的字符串长度记录等有重复字符串的时候将前面的截取 比对长度记录最大值 从这个字符开始继续前面的操作 O(n)

2.1 代码

var lengthOfLongestSubstring = function(s) {
    let subs = '';
    let res = 0;
    for (let i = 0; i < s.length; i++) {
        //判断是否包含重复字符串
        const index = subs.indexOf(s[i]);
        //如果有重复的字符串
        if (index !== -1) {
            //比对 记录这个长度
            res = Math.max(res, subs.length);
			//截取对应的字符串(将对应的字符串前面的内容取出 拿出重复的内容)
            subs = subs.substr(index + 1);
        }
        //字符串拼接
        subs += s[i];
    }
    //最后比对最大值返回
    return Math.max(res, subs.length);
};

2.2 代码

var lengthOfLongestSubstring = function(s) {
    let minIndex = 0
    let count = 0
    //遍历
    for(let i = 0; i < s.length; i++){
        //判断是否重复项之前是否出现 最小下标前移
        if(s.indexOf(s[i], minIndex) < i) minIndex = s.indexOf(s[i], minIndex) + 1
        //没有找到 给长度赋值
        else count = Math.max(count, i - minIndex + 1)
    }
    //返回长度
    return count
};

2.3 代码

var lengthOfLongestSubstring = function(s) {
    //准备一个数组来保存对应的字符串
    let arr = []
    //记录最大长度
    let max = 0
    //遍历字符串
    for (let i = 0; i < s.length; i ++) {
        //判断数组中是否有重复元素
        let index = arr.indexOf(s[i]);
        //如果找到了对应的元素删除之前的内容
        index > -1 && arr.splice(0, index + 1)
        //将对应的字符串添加到数组
        arr.push(s[i])
        //获取最大值
        max = Math.max(arr.length, max)
    }
    return max;
};

提交截图(以上三种的思想和速度差不多)

 

 

4.最长回文子串

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

示例 1:

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

示例 2:

输入:s = "cbbd"
输出:"bb"

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

解题

回文字概述

从两边向中间,不断比较头尾字符是否相同,也就是对应的内容是否对称。

 

中心扩展方法

中心扩展方法的思路:

遍历每一个字符,向两边扩展找到以其为中心的最长回文子串, 最后比较所有的回文字的长度得到最长。

 

回文字也分俩种情况

一种是奇数

 

一种是偶数

 

实现代码

//中心扩展函数 参数为字符串s 左下标l 有下标r
function search(s,l,r){
    let left=l;
    let right=r;
    //判断是否在区间 以及对应的左指针和右指针是否指向同一个值
    while(left>=0&&right<s.length&&s[left]===s[right]){
        //左指针前移
        left--;
        //右指针前移
        right++;
    }
    //返回对应的长度 最大下标-最小下标-1
    return right-left-1;
}
//本题主函数
function longestPalindrome(s){
    //默认开始位置和结束位置为0
    let start=end=0;
    //循环遍历字符串
    for(let i=0;i<s.length;i++){
        //中心扩展 奇数情况
        let count1=search(s,i,i);
        //中心扩展 偶数情况
        let count2=search(s,i,i+1);
        //得到俩种扩展的最大长度
        let len=Math.max(count1,count2);
        //如果对应的长度超出开始位置和结束位置
        if(len>end-start){
            //开始位置前移
            start=i-Math.floor((len-1)/2);
            //结束位置后移
            end=i+Math.floor((len/2));
        }
    }
    //返回对应的字符串
    return s.slice(start,end+1);
};

提交截图

 

 

马拉车算法 Manacher

Manachar算法主要是处理字符串中关于回文串的问题的,它可以在 O(n) 的时间处理出以字符串中每一个字符为中心的回文串半径,由于将原字符串处理成两倍长度的新串,在每两个字符之间加入一个特定的特殊字符,因此原本长度为偶数的回文串就成了以中间特殊字符为中心的奇数长度的回文串了。

如图:我们可以在每个字符间插入"#",并且为了使得扩展的过程中,到边界后自动结束,在两端分别插入 "^" 和 "$",两个不可能在字符串中出现的字符,这样中心扩展的时候,判断两端字符是否相等的时候,如果到了边界就一定会不相等,从而出了循环。经过处理,字符串的长度永远都是奇数了。

首先我们用一个数组 P 保存从中心扩展的最大个数,而它刚好也是去掉 "#" 的原字符串的总长度。例如下图中下标是 6 的地方。可以看到 P[ 6 ] 等于 5,所以它是从左边扩展 5 个字符,相应的右边也是扩展 5 个字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢复到原来的字符串,变成 "cbcbc",它的长度刚好也就是 5。

求原字符串下标

用 P 的下标 i 减去 P [ i ],再除以 2 ,就是原字符串的开头下标了。

例如我们找到 P[ i ] 的最大值为 5 ,也就是回文串的最大长度是 5 ,对应的下标是 6 ,所以原字符串的开头下标是 (6 - 5 )/ 2 = 0 。所以我们只需要返回原字符串的第 0 到 第 (5 - 1)位就可以了。

求每个 P [ i ]

接下来是算法的关键了,它充分利用了回文串的对称性。

我们用 C 表示回文串的中心,用 R 表示回文串的右边半径。所以 R = C + P[ i ] 。C 和 R 所对应的回文串是当前循环中 R 最靠右的回文串。

让我们考虑求 P [ i ] 的时候,如下图。

用 i_mirror 表示当前需要求的第 i 个字符关于 C 对应的下标。

我们现在要求 P [ i ], 如果是用中心扩展法,那就向两边扩展比对就行了。但是我们其实可以利用回文串 C 的对称性。i 关于 C 的对称点是 i_mirror ,P [ i_mirror ] = 3,所以 P [ i ] 也等于 3 。

但是有三种情况将会造成直接赋值为 P [ i_mirror ] 是不正确的,下边一一讨论。

1. 超出了 R

当我们要求 P [ i ] 的时候,P [ mirror ] = 7,而此时 P [ i ] 并不等于 7 ,为什么呢,因为我们从 i 开始往后数 7 个,等于 22 ,已经超过了最右的 R ,此时不能利用对称性了,但我们一定可以扩展到 R 的,所以 P [ i ] 至少等于 R - i = 20 - 15 = 5,会不会更大呢,我们只需要比较 T [ R+1 ] 和 T [ R+1 ]关于 i 的对称点就行了,就像中心扩展法一样一个个扩展。

2. P [ i_mirror ] 遇到了原字符串的左边界

此时P [ i_mirror ] = 1,但是 P [ i ] 赋值成 1 是不正确的,出现这种情况的原因是 P [ i_mirror ] 在扩展的时候首先是 "#" == "#" ,之后遇到了 "^"和另一个字符比较,也就是到了边界,才终止循环的。而 P [ i ] 并没有遇到边界,所以我们可以继续通过中心扩展法一步一步向两边扩展就行了。

3. i 等于了 R

此时我们先把 P [ i ] 赋值为 0 ,然后通过中心扩展法一步一步扩展就行了。

考虑 C 和 R 的更新

就这样一步一步的求出每个 P [ i ],当求出的 P [ i ] 的右边界大于当前的 R 时,我们就需要更新 C 和 R 为当前的回文串了。因为我们必须保证 i 在 R 里面,所以一旦有更右边的 R 就要更新 R。

此时的 P [ i ] 求出来将会是 3 ,P [ i ] 对应的右边界将是 10 + 3 = 13,所以大于当前的 R ,我们需要把 C 更新成 i 的值,也就是 10 ,R 更新成 13。继续下边的循环。

代码

//插入字符解决奇偶问题
function preProcess(s) {
    //获取字符串长度
    let n = s.length;
    //如果当前长度为0
    if (n == 0) {
        return "^$";
    }
    //接收新字符串变量 开头为^
    let ret = "^";
    //遍历添加#号
    for (let i = 0; i < n; i++){
        ret = ret + "#" + s.charAt(i);
    }
    //添加结束
    ret = ret + "#$";
    //返回新字符串
    return ret;
}
// 马拉车算法
function longestPalindrome(str) {
    //接收新字符串
    let S = preProcess(str);
    let n = S.length;// 保留新字符串的长度
    let len = new Array(n);//新建一个长度为n的数组
    let center = 0, right = 0;// 初始回文串中心点击 及右边界
    // 从第 1 个字符开始
    for (let i = 1; i < n - 1; i++) {
        // 找到i后面的对称位
        let mirror = 2 * center - i;
        //判断i在右边界范围内
        if (right > i) {
            // i 在右边界的范畴内,看看i的对称点的回文串长度,以及i到右边界的长度,取两个较小的那个
            // 不能溢出之前的边界,否则就得核心拓展
            len[i] = Math.min(right - i, len[mirror]);
        } else {
            // 超过范畴了,核心拓展
            len[i] = 0;
        }
        
        // 核心拓展 如果对应的俩边是回文 长度+1
        while (S.charAt(i + 1 + len[i]) == S.charAt(i - 1 - len[i])) {
            len[i]++;
        }

        //如果对应的位置加长度超出右边界
        if (i + len[i] > right) {
            // 更新核心
            center = i;
            // 更新右边界
            right = i + len[i];
        }
    }
    // 通过回文长度数组找出最长的回文串
    let maxLen = 0;
    let centerIndex = 0;
    //循环遍历
    for (let i = 1; i < n - 1; i++) {
        //如果长度大于当前的最大长度 重新赋值
        if (len[i] > maxLen) {
            maxLen = len[i]; //将长度赋给最大值
            centerIndex = i; //中心位置变化
        }
    }
    //原字符串的开始位置
    let start = (centerIndex - maxLen) / 2;
    //截取返回
    return str.substring(start, start + maxLen);
}

提交截图

 

 

动态规划(DP)此处效率过低

/**
 * @param {string} s
 * @return {string}
 */ 
var longestPalindrome = function(s){
   let lplang = 1 // 最长长度
    let start = 0 // 当最长长度更新时同时缓存起始点

    let len = s.length
    
    let dp = [] // 缓存结果
    
    // 长度 1 2 单独处理
    for(let i=0; i<len; i++) {
        if(dp[i] === undefined) {
            dp[i] = [] // 初始化二维数组
        }
        dp[i][i] = true // 自己是自己的回文
        // 找出长度为2的回文
        if(s[i] === s[i+1]) {
            dp[i][i+1] = true // 缓存结果
            start = i
            lplang = 2 // 比如两个连续的 aa 也是回文,并且标记最长长度
        }
    }
    
    // 动态规划 找最优子,比如 aa 是回文,那么 baab 只需要判断 b === b && (aa: 缓存的结果)
    // L 为子串长度,首先找出长度为3的回文,然后是4,5...
    // 同时通过 L 计算 子串终点 i+L-1,起始点+长度-1 = 终点
    for(let L=3; L<=len; L++) {
        for(let i=0; i+L-1<len; i++) {
                // 越往后便利次数越少 当L是16的时候 只遍历1次 判断s[0]s[15]是否相等
            let j = i+L-1 // 第一次对比的是 s[0] === s[2]
            if(s[i] === s[j] && dp[i+1][j-1]) {
                // 两头相等 并且 子是回文
                dp[i][j] = true // 缓存结果 先缓存的是所有的最短的子串结果
                start = i
                lplang = L // 因为L是从小到大便利的 所以后面的回文长度就是最长的
            }
        }
    }
    return s.substr(start,lplang)
}

提交截图

 


帮助到你的话就点个关注吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值