环形链表衍生问题——定位环的起点
法一:记录第一次遇到的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;
};