从零开始学算法的第一天~(二分查找,双指针,两数相加,最长回文子串)

前言

元旦放假,😅忘记把公司写的代码push到代码仓库了,导致react系列没法更新。于是就开个新坑算法系列。

作为一个前端开发,数据结构和算法 一直是我最大的短板,也是心里很大的一个障碍,当初有尝试着刷leetcode,但是被一道题卡着一天,直到怀疑自己是不是智商有问题 的那种感觉至今还记忆犹新。

但是昨天晚上睡觉前 冴羽 大佬突然加了我微信,他看了我的年终总结并且给了我一些鼓励,我备受振奋,整晚都热血沸腾哈哈哈~

今天就是我 硬钢算法的第一天,我的基础真的很差。文章重在记录总结,我也很好奇我会坚持多久,我只有以写文章的形式记录每天刷的题才能让自己保持一个积极的态度,不然每次看一会就懈怠了😭。只有看到大家的反馈我才能源源不断的坚持下去!

刷题

由于我的基础不好,我是从leetcode中的 算法入门 开始刷的,同时搭配一些 面试常见题,一边补基础,一边巩固。主要是总结结题时的心得。

题目的描述由于篇幅原因没有写上,可以直接点击标题链接进 leetcode 里看

704.二分查找

二分查找的话我之前有了解过的,核心思想在于折半查找,不断缩小查询范围

主要适用于数据 按顺序排列,并且 数据的状态只用两种的情况,比如 对或错,白天或黑天 的情况。

我实现的代码如下:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    if(!nums.length)return -1
    let left = 0
    let right = nums.length - 1
    while(left <= right){
        const mid = Math.floor(( right - left)/2 + left)
        if(nums[mid] > target){
            right = mid - 1
        }
        else if(nums[mid] < target){
            left = mid + 1
        }
        else{
            return mid
        }
    }
    return -1
};

在实现的过程中,根据正确解法,中间值需要使用 ( right - left ) / 2 + left 来取,因为直接使用 (left + right) / 2 有可能和的数字太大,导致内存溢出。

还有一个点是边界条件的问题,循环的边界条件 取开还是取闭 决定于我们想要找的值是否会在 left == right的情况下获取。

278. 第一个错误的版本

根据题目条件判断使用二分查找,我完成的代码如下:

/**
 * Definition for isBadVersion()
 * 
 * @param {integer} version number
 * @return {boolean} whether the version is bad
 * isBadVersion = function(version) {
 *     ...
 * };
 */

/**
 * @param {function} isBadVersion()
 * @return {function}
 */
var solution = function(isBadVersion) {
    /**
     * @param {integer} n Total versions
     * @return {integer} The first bad version
     */
    return function(n) {
        let left = 1
        let right = n
        while(left < right){
            const mid = Math.floor( left +(right - left)/2) 
            if(isBadVersion(mid)){
                right = mid
            }
            else{
                left = mid + 1
            }
        }
        return right
    };
};

这里有两个注意点,一个是边界 left < right,这里说明我们的 left == right 是不需要执行什么操作的,在这种情况下数组为[true,false] , right 已经是第一个错误版本了,不需要多走一次循环。

另一个是 right = mid,之所以是 mid 而不是 mid - 1,是因为第一个错误的版本肯定是在中间值的左侧,所以 中间值有可能是正确版本

35. 搜索插入位置

根据题目条件使用二分查找,我实现的代码如下:

var searchInsert = function(nums, target) {
    const n = nums.length;
    let left = 0, right = nums.length - 1;
    if(target > nums[right]){
        return right + 1
    }
    while (left < right) {
        let mid = Math.floor((right - left)/2) + left;
        if (target <= nums[mid]) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return right;
};

这一题的也有一些注意点,首先是如果传入的数字大于数组最大的值就直接返回数组长度,这个是我在提交失败之后才改的😂。还有一点是

977. 有序数组的平方

这一题使用的是双指针来解决,首先创建一个新的空数组,遍历原数组,指定两个指针leftright初始值是数组的头和尾。

遍历的过程中leftright 指向的值分别平方运算后再对比大小,将大的值移入新数组,并将指针向另一侧移动,直到两个指针相遇。

var sortedSquares = function(nums) {
    const arr = []
    let left = 0
    let right = nums.length - 1
   while(left <= right){
       const leftNum = nums[left] * nums[left]
       const rightNum = nums[right] * nums[right]
       if(leftNum > rightNum){
           arr.unshift(leftNum)
           left ++
       }
       else{
           arr.unshift(rightNum)
           right --
       }
   }
   return arr
};

189. 轮转数组

这一题原本是在双指针的题型下的,但是我自己用了一个玄学方法解决了😂

我将数组以 nums.length - k 为边界分为两组,然后交换两组的位置为新数组就解决了。但是这里有一个问题,我原本只需要将 arr 返回即可,但是 nums 可能为引用类型,如果我使用不可变的赋值方式的话leetcode会识别不到我的操作,我只能遍历nums一个个的去修改。

代码如下:

var rotate = function(nums, k) {
    k = k % nums.length
    const left = nums.slice(0,nums.length - k)
    const right = nums.slice(nums.length -k,nums.length)
    const arr = [...right,...left]
    for(let i in nums){
        nums[i] = arr[i]
    }
};

2. 两数相加

这一题折磨了我好久,主要是以前不知道js是如何实现链表的,看了题解半天都不理解很多操作和方法是哪来的😤

后面去补了一下js实现链表的知识才看懂并自己实现了一次。代码如下:

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    let sumList = null 
    let tail = null
    let carry = 0
    while(l1||l2){
        num1 = l1?l1.val:0
        num2 = l2?l2.val:0
        let sum = num1 + num2 + carry
        if(sum >= 10){
            carry = Math.floor(sum / 10) 
            sum = sum % 10
        }
        else{
            carry = 0
        }
        if(sumList){
            tail.next = new ListNode(sum)
            tail = tail.next
        }
        else{
            sumList = tail = new ListNode(sum)
        }
        l1 = l1?l1.next:l1
        l2 = l2?l2.next:l2
    }
    if(carry > 0){
        tail.next = new ListNode(carry)
    }
    return sumList
};

在js中是没有链表这种数据结构的,需要自己实现一个类来构造,leetcode中链表中的每一个链表节点的结构大概如下

1.  class ListNode {
1.    constructor(value, next = null){
1.      this.value = value;
1.      this.next = next;
1.    }
1.  }

而一整条链表就是一个节点接着一个节点来实现的,连接他们的就是 next 这个属性,我们可以使用表头的那个节点代表整个链表。其实实现的逻辑和js中原型链差不多,如下案例

const head = new ListNode(1)
head.next = tail1 = new ListNode(2)

那么要遍历链表的话就可以通过判断next是否存在来,进行一个 while遍历

// head是一个链表
const tail = head
while(tail.next){
    // do something
    tail = tail.next
}

5.最长回文子串

这题琢磨了好久哈哈哈,看了下解题思路才自己摸索出来。

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    let arr = s.split('')
    if(s.length < 2){
        return arr[0]
    }
    let maxLeft = 0
    let maxRight = 0
    let maxIndex = 0
    for(let i = 0;i < arr.length;i++){
        let leftOffset = 1
        let rightOffset = 1
        while(arr[i - leftOffset] === arr[i]){
            leftOffset++
        }
        while(arr[i + rightOffset] === arr[i]){
            rightOffset++
        }
        
        
        while(arr[i - leftOffset]&&arr[i + rightOffset]){
            if(arr[i - leftOffset] === arr[i + rightOffset]){
                leftOffset++
                rightOffset++
            }
            else{
                break
            }
        }
        
        if(rightOffset + leftOffset - 2 > maxRight + maxLeft ){
            maxRight = rightOffset - 1
            maxLeft = leftOffset - 1
            maxIndex = i
        }
        
    }
    return arr.slice(maxIndex - maxLeft,maxIndex + maxRight + 1).join('')
};

我的实现方法是设定左右两个范围,先遍历整个字符串,然后从当前位置分别 向左和向右去遍历,如果左边或者右边是和当前字符串相同的值就将范围扩大。这一步是为了判断相邻字符串相同的情况,例如 bbb 这种字符串。

代码如下

let leftOffset = 1
let rightOffset = 1
while(arr[i - leftOffset] === arr[i]){
    leftOffset++
}
while(arr[i + rightOffset] === arr[i]){
    rightOffset++
}

在将重复的情况处理好之后,再遍历判断当前位置左右两侧的值是否相等,如果相等就再扩大范围。

代码如下

while(arr[i - leftOffset]&&arr[i + rightOffset]){
            if(arr[i - leftOffset] === arr[i + rightOffset]){
                leftOffset++
                rightOffset++
            }
            else{
                break
            }
        }

每次外层遍历结束前判断一下本次遍历的左右范围大小是否是最大的,如果是最大的就替换一下最大值

if(rightOffset + leftOffset - 2 > maxRight + maxLeft ){
    maxRight = rightOffset - 1
    maxLeft = leftOffset - 1
    maxIndex = i
}

最后返回一个字符串,maxIndex指的是遍历时 回文子串的基准index值

return arr.slice(maxIndex - maxLeft,maxIndex + maxRight + 1).join('')

总结

因为是休息日所以有时间可以刷的比较多,后面争取每天至少两题的频率坚持下去,并且持续产出文章以确保自己能够理解题目,毕竟文章误导别人也是一件很丢脸的事情哈哈哈~

非常非常欢迎你点个赞或者写个评论让我知道有人在监督我,避免我偷懒!,如果你对算法学习有一定心得也欢迎你为我提供帮助哈~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值