JS算法练习3.10

本文探讨了JavaScript中与算法和数据结构相关的多种问题,包括环形链表中环的起点定位、有效括号的判断、每日温度变化的处理、最小栈的实现、使用栈或队列解决滑动窗口最大值问题,以及反转字符串中元音字母的方法。通过不同的解题策略,如双指针、栈和队列等,展示了JavaScript在算法应用上的灵活性。
摘要由CSDN通过智能技术生成

环形链表衍生问题——定位环的起点

法一:记录第一次遇到的flag已存在的节点

function detectCycle(head) {

    while (head) {
        if (head.flag) return head;
        else {
            head.flag = true;
            head = head.next;
        }
    }

    return null;
}

法二:双指针

定义慢指针 slow,快指针 fast。两者齐头并进, slow 一次走一步、fast 一次 走两步。这样如果它们是在一个有环的链表里移动,一定有相遇的时刻。这个原理证明起来也比较简单:我们假设移动的次数为 t,slow 移动的路程就是t,fast 移动的路程为2t,假如环的长度为 s,那么当下面这个条件:

2t - t = s

也就是:

t = s

满足时,slow 和 fast 就一定会相遇。反之,如果两者没有相遇,同时 fast 遍历到了链表的末尾,发现 next 指针指向 null,则链表中不存在环。

有效括号

const leftToRight = {
    "(": ")",
    "[": "]",
    "{": "}"
}; //map

function isValid(s) {

    if (!s) return true; //空串无条件为true

    const stack = [];

    for (let i = 0; i < s.length; i++) {

        const ch = s[i];

        if (ch === "(" || ch === "{" || ch === "[") stack.push(leftToRight[ch]);
        else { //若不是左括号,则必须是和栈顶的左括号相配对的右括号
            if (!stack.length || stack.pop() !== ch) {
                return false; //若栈不为空,且栈顶的左括号没有和当前字符匹配上,那么判为无效
            }
        }

    }
    return !stack.length; //若所有的括号都能配对成功,那么最后栈应该是空的
}

Leetcode 739 每日温度

暴力遍历法:直接两层遍历,第一层定位一个温度,第二层定位离这个温度最近的一次升温是哪天,然后求出两个温度对应索引的差值即可。

避免重复操作的秘诀就是及时地将不必要的数据出栈,避免它对我们后续的遍历产生干扰。拿这道题来说,我们的思路就是:尝试去维持一个递减栈。
当遍历过的温度,维持的是一个单调递减的态势时,我们就对这些温度的索引下标执行入栈操作;只要出现了一个数字,它打破了这种单调递减的趋势,也就是说它比前一个温度值高,这时我们就对前后两个温度的索引下标求差,得出前一个温度距离第一次升温的目标差值。

/**
 * @param {number[]} temperatures
 * @return {number[]}
 */
var dailyTemperatures = function(T) {

    const len = T.length;
    const stack = [];
    const res = (new Array(len)).fill(0);

    for (let i = 0; i < len; i++) {
        while (stack.length && T[i] > T[stack[stack.length - 1]]) { 
            //若栈不为0,且存在打破递减趋势的温度值
            const top = stack.pop() //将栈顶温度值对应的索引出栈  
            res[top] = i - top //计算当前栈顶温度值与第一个高于它的温度值 的索引差值 
        }
        stack.push(i) //注意栈里存的不是温度值,而是索引值,这是为了后面方便计算

    }
    
    return res;
}

Leetcode 155 最小栈

法一:O(n)

const MinStack = function() {
    this.stack = []
};

MinStack.prototype.push = function(x) {
    this.stack.push(x)
};

MinStack.prototype.pop = function() {
    this.stack.pop()
};

MinStack.prototype.top = function() {
    if (!this.stack || !this.stack.length) {
        return
    }
    return this.stack[this.stack.length - 1]
};

//按照一次遍历的思路取最小值
MinStack.prototype.getMin = function() {
    let minValue = Infinity
    const { stack } = this
    for (let i = 0; i < stack.length; i++) {
        if (stack[i] < minValue) {
            minValue = stack[i]
        }
    }
    return minValue
};

法二:O(1)

const MinStack2 = function() {
    this.stack = [];
    // 定义辅助栈
    this.stack2 = [];
};

MinStack2.prototype.push = function(x) {
    this.stack.push(x);
    // 若入栈的值小于当前最小值,则推入辅助栈栈顶
    if (this.stack2.length == 0 || this.stack2[this.stack2.length - 1] >= x) {
        this.stack2.push(x);
    }
};

MinStack2.prototype.pop = function() {
    // 若出栈的值和当前最小值相等,那么辅助栈也要对栈顶元素进行出栈,确保最小值的有效性
    if (this.stack.pop() == this.stack2[this.stack2.length - 1]) {
        this.stack2.pop();
    }
};

MinStack2.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

MinStack2.prototype.getMin = function() {
    // 辅助栈的栈顶,存的就是目标中的最小值
    return this.stack2[this.stack2.length - 1];
};

用栈实现队列

const MyQueue = function() {
    this.stack1 = [];
    this.stack2 = [];
}

MyQueue.prototype.push = function(x) {
    this.stack1.push(x);
};

MyQueue.prototype.pop = function() {
    if (this.stack2.length <= 0) { //假如 stack2 为空,需要将 stack1 的元素转移进来
        while (this.stack1.length !== 0) { //当 stack1 不为空时,出栈
            this.stack2.push(this.stack1.pop()); //将 stack1 出栈的元素推入 stack2
        }
    }
    return this.stack2.pop(); //为了达到逆序的目的,我们只从 stack2 里出栈元素
};

//这个方法和 pop 唯一的区别就是没有将定位到的值出栈
MyQueue.prototype.peek = function() {
    if (this.stack2.length <= 0) {
        while (this.stack1.length != 0) {
            this.stack2.push(this.stack1.pop());
        }
    }
    const stack2Len = this.stack2.length;
    return stack2Len && this.stack2[stack2Len - 1];
};


MyQueue.prototype.empty = function() {
    return !this.stack1.length && !this.stack2.length;
};

Leetcode 239 滑动窗口最大值

法一:双指针遍历 O(kn)

function maxSlidingWindow(nums, k) {
    const res = [];
    const len = nums.length;
    let left = 0,
        right = k - 1;

    while (right < len) {
        const max = calMax(nums, left, right);
        res.push(max);
        left++, right++;
    }

    return res;
}

function calMax(arr, left, right) {
    if (!arr || !arr.length) return;
    let maxNum = arr[left];
    for (let i = left; i <= right; i++)
        if (arr[i] > maxNum) maxNum = arr[i];
    return maxNum;
}

法二:双端队列 O(n)

  • 检查队尾元素,看是不是都满足大于等于当前元素的条件。如果是的话,直接将当前元素入队。否则,将队尾元素逐个出队、直到队尾元素大于等于当前元素为止。
  • 将当前元素入队
  • 检查队头元素,看队头元素是否已经被排除在滑动窗口的范围之外了。如果是,则将队头元素出队。
  • 判断滑动窗口的状态:看当前遍历过的元素个数是否小于 k。如果元素个数小于k,这意味着第一个滑动窗口内的元素都还没遍历完、第一个最大值还没出现,此时我们还不能动结果数组,只能继续更新队列;如果元素个数大于等于k,这意味着滑动窗口的最大值已经出现了,此时每遍历到一个新元素(也就是滑动窗口每往前走一步)都要及时地往结果数组里添加当前滑动窗口对应的最大值(最大值就是此时此刻双端队列的队头元素)。

这四个步骤分别有以下的目的:

  • 维持队列的递减性:确保队头元素是当前滑动窗口的最大值。这样我们每次取最大值时,直接取队头元素即可。
  • 这一步没啥好说的,就是在维持队列递减性的基础上、更新队列的内容。
  • 维持队列的有效性:确保队列里所有的元素都在滑动窗口圈定的范围以内。
  • 排除掉滑动窗口还没有初始化完成、第一个最大值还没有出现的特殊情况。
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
const maxSlidingWindow = function (nums, k) {
  // 缓存数组的长度
  const len = nums.length;
  // 初始化结果数组
  const res = [];
  // 初始化双端队列
  const deque = [];
  // 开始遍历数组
  for (let i = 0; i < len; i++) {
    // 当队尾元素小于当前元素时
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      // 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
      deque.pop();
    }
    // 入队当前元素索引(注意是索引)
    deque.push(i);
    // 当队头元素的索引已经被排除在滑动窗口之外时
    while (deque.length && deque[0] <= i - k) {
      // 将队头元素索引出队
      deque.shift();
    }
    // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  // 返回结果数组
  return res;
};

Leetcode 345 反转字符串中的元音字母

双指针法

/**
 * @param {string} s
 * @return {string}
 */
var reverseVowels = function(s) {
    const n = s.length;
    const arr = Array.from(s);
    let i = 0, j = n - 1;
    while (i < j) {
        while (i < n && !isVowel(arr[i])) {
            ++i;
        }
        while (j > 0 && !isVowel(s[j])) {
            --j;
        }
        if (i < j) {
            swap(arr, i, j);
            ++i;
            --j;
        }
    }
    return arr.join('');
}

const isVowel = (ch) => {
    return "aeiouAEIOU".indexOf(ch) >= 0;
}

const swap = (arr, i, j) => {
    const temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

Leetcode 11 盛最多水的容器

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(arr) {
    let left=0,right=arr.length-1;
    let ans=0;
    while(left<right){
        let area=Math.min(arr[left],arr[right])*(right-left);
        ans=Math.max(ans,area);
        if(arr[left]<=arr[right]) left++;
        else right--;
    }

    return ans;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值