一周力扣刷题笔记

一周力扣刷题笔记(10月31日)

这周的刷题主要是针对于栈的内容进行的,也包括了一些对于链表知识的整理与回顾

旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

这道题最开始的基本思路是分成两部分操作,向右移动的话,只要将后面k个位置的节点移动到头节点,就可以了。但这种方法的边界条件过多,而且由于要多次操作整个链表,因此要定义多个头指针来遍历数组,非常麻烦。最重要的是,当K远于链表长度,这种方式是无法处理的(失败版本)

var rotateRight = function(head, k) {
let len = 0;
let len2 = 0;
let res = head;
let res2 = head;
if (head == null || head.next == null) { return head }
if (k < 2) { return head }
while (res2.next != null) {
    len2++;
    res2 = res2.next;
}
len2++;
k = k % len2;
while (len != k) {
    len++;
    res = res.next
}
let temp = res.next;
res.next = null;
let tem = temp
while (tem.next != null) {
    tem = tem.next
}
tem.next = head;
return temp
}

更好的做法是采用循环链表的方式,直接重新定义头节点,利用环形链表的特定,直接将已经遍历到为尾部的指针重新连接到头部,通过操作指针的方式,找到移动过后的头节点,再进行分割,就达到了后移的目的,同时,利用环形链表的节点数可以取余也有效的解决了K值大于链表长的问题

var rotateRight = function(head, k) {
if (k === 0 || !head || !head.next) {
    return head; //除去边界条件
}
let n = 1;
let cur = head;
// 先遍历出链表的长度
while (cur.next) {
    cur = cur.next;
    n++;
}

let add = n - k % n; //注意环形链表
// 移动整数倍直接返回头节点
if (add === n) {
    return head;
}
//    使链表称为一个环形链表
cur.next = head;
//将节点后移,利用环形链表直接后移
while (add) {
    cur = cur.next;
    add--;
}
const ret = cur.next;
// 利用cur.next=null将环形链表分割
cur.next = null;
return ret;
};

两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

这道题的难度并不大,主要是要理清楚交换两个节点的顺序,对于单向链表而言,由于只能访问大后面的节点,因此操作节点一定要从后往前进行操作,防止出现节点丢失的问题

var swapPairs = function(head) {
let headnode = new ListNode(); //重新定义一个空的头节点用于辅助
headnode.next = head;
let res = headnode;
console.log(res)
while (res.next != null && res.next.next != null) { //(temp.next && temp.next.next) 条件可以写成这种形式
    let tmp = res.next.next; //先保存后一位的节点
    res.next.next = res.next.next.next; //将前节点的next连接到后一位节点的next
    tmp.next = res.next;    //将后节点放到前节点之前
    res.next = tmp; //将后一位节点连接到前节点的前一位(辅助节点的下一位)
    res = res.next.next;  //指针往后移动两位(每次操作了两位)
}
return headnode.next;
};

删除排序链表中的重复元素

** 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。**

这道题目相对比较简单,基本就是一般的遍历循环并判断,但要注意的一点是,在这道题里如果发生了节点删除,指针是不移动到下一位的,否则当出现连续的重复元素时,指针在第一次删除元素后跳到了剩余的相同元素的下一位,相当于跳过了一个相同元素,最后就会出现重复元素消除不彻底的情况

var deleteDuplicates = function(head) {
let res = head;
while (1) {
    if (!res || !res.next) { break; }
    if (res.next.val == res.val) {
        res.next = res.next.next;
    } else { //注意这里要有else,否则多个连续的重复数字无法去重
        res = res.next;
    }
}
return head;
}

删除排序链表中的重复元素2

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。 返回同样按升序排列的结果链表。

跟前面的题目类似,但难度明显大了很多,这道题要求去掉所有链表中发生过重复的元素,这里不仅仅是要多考虑后面一个节点,还要考虑连续重复的节点需要一直舍弃的问题,因此当出现重复元素时先保存这个值,然后一直往后遍历直到后面的值与这个保存的值不相等为止,在这个过程中迭代指针去掉所有重复的元素(指针最后指向的是不重复的元素)

  var deleteDuplicates = function(head) {
    let headnode = new ListNode();
    headnode.next = head;
    let res = headnode;
    let las = res;
    let temp;
    if (!res.next) { return res.next }
    while (true) {
        if (!res.next.next) { break; } //后面只有一个元素必不可能重复
        // 这里要跟下面对应起来,会进行贪婪去重,当循环结束时指针以及指针前面一定没有重复元素
        if (res.next.val == res.next.next.val) {
            temp = res.next.val;
            while (res.next.val == temp) {
                res.next = res.next.next;
                // 循环指针直到没有重复元素为止,注意此时最后最后一次去完后,后面哪一项一定不与前面的重复
                if (!res.next) { return las.next }
            }
        } else {
            res = res.next;
            // 继续指向下一个节点
        }
    }
    return las.next;
	};

分割链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。

这道题我最开始的基本思路是,由于只要让所有小的出现在一边,大的出现在另一边,那么在一次遍历时,定义两个链表,一个存放比x小的所有节点,另一个存放比x大的所有节点,最后将两个链表和x拼接起来就可以了。

   var partition = function(head, x) {
    let onehead = new ListNode(0);
    onehead.next = head;
    head = onehead;
    let res = head;
    let lastnode = new ListNode(0);
    let last = lastnode;
    let headnode = new ListNode(0);
    let bef = headnode;
    while (1) {
        if (res.val == x) {
            last.next = null;
            break;
        }
        if (res.next.val > x) {
            last.next = res.next;
            last = last.next;
            res.next = res.next.next;
        }
        res = res.next
    }
    while (1) {
        if (!res.next) { break; }
        if (res.next.val < x) {
            bef.next = res.next;
            bef = bef.next;
            res.next = res.next.next;
        } else {
            res = res.next
        }
    }
    head = head.next;
    lastnode = lastnode.next;
    bef.next = head;
    res.next = lastnode;
    return headnode.next
	};

这种方式是能够在多数情况符合要求的,但少数情况下会由于这个代码保留了x两边的节点,导致一些时候没有保留相对位置,其实正确的解法跟我的思路基本是一样的,但他仅仅拆分成立小于x和大于等于x的部分,在简化代码的同时也保留了相对位置,防止了边界条件。同时,下面这个程序巧妙地运用了=从右往左计算的性质,只用一个指针遍历了一遍链表,效率更高

var partition = function(head, x) {
let pA = a = new ListNode(0),
    pB = b = new ListNode(0)		//利用等号处理简化代码
while (head) {
    head.val < x ? a = a.next = head : b = b.next = head
        //利用等号的处理顺序简化代码
    head = head.next
}
a.next = pB.next
b.next = null //注意将后面链表的next置空
return pA.next
};

所有显然我考虑问题还不够周全,差了一点点
##简化路径
给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 ‘/’ 开头),请你将其转化为更加简洁的规范路径。

  • 在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (…) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,’//’)都被视为单个斜杠 ‘/’ 。 对于此问题,任何其他格式的点(例如,’…’)均被视为文件/目录名称。

  • 请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 ‘/’ 开头。

  • 两个目录名之间必须只有一个斜杠 ‘/’ 。

  • 最后一个目录名(如果存在)不能 以 ‘/’ 结尾。
    *此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 ‘.’ 或 ‘…’)。

  • 返回简化后得到的 规范路径 。

这道题明显是要我们使用栈进行解题,按照linux的规则,遇到文件名进栈;遇到’…/‘出栈,遇到’./'直接继续遍历,注意文件名是多个字符串,要先对字符串数组做一些处理,转化为文件名和特殊符号的形式,最后还有对边界条件进行处理

var simplifyPath = function(path) {
path = path.split("");
if (path[path.length - 1] != '/') { path.push('/') }
let arr = [];
let stk = [];
let str = '';
// 先对数组进行处理,将其转变为/与文件名/特殊符号的形式
for (let i = 0; i < path.length; i++) {
    if (path[i] == '/') {
        if (str != '') { //注意文件名是多个字符,需要先保存再一并 加入
            arr.push(str);
            str = '';
        }
        continue;
    } else {
        str += path[i];
    }
}
console.log(arr)
for (let i = 0; i < arr.length; i++) {
    if (arr[i] != "." && arr[i] != '..') {
        stk.push(arr[i]); //不等于特殊符号默认文件名,直接进栈
    } else if (arr[i] == ".") {
        continue; //遇到'./'同级文件,跳过
    }
    if (arr[i] == '..' && stk != []) {
        stk.pop(); //遇到../出栈,注意判断非空条件
    }
}
// 最后加入/并对边界条件做一些处理
let las = stk.join("/").split("");
if (las[0] != '/') { las.unshift("/") }
return las.join("")
};	

这个算法还可以优化,比如处理字符串的那一步,我们可以直接通过split这个API进行处理,对循环和边界条件的处理的写法也可以化简

   const dir = path.split('/'),
    stack = []
	for (const i of dir) {
    // 特殊情况直接跳过
    if (i === '.' || i === '') continue
        // 遇到'..'入栈
    if (i === '..') {
        stack.length > 0 ? stack.pop() : null
        continue
    }
    //其他情况直接进栈
    stack.push(i)
	}
	// 直接在return处处理结果
	return '/' + stack.join('/')

有效的括号

** 给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。**

  • 有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。

  • 左括号必须以正确的顺序闭合。

还是经典的栈的运用,不管我们右多少括号,处理的时候只要处理栈顶就可以了,如果匹配到左括号就入栈,匹配到右括号就跟栈顶进行比较,如果跟栈顶的左括号无法匹配,就说明匹配失败,这组括号是非法的。否则就将栈顶出栈,如果最后栈空,说明一一匹配完毕,括号组合法。

对于代码的编写,我们可以选择使用map进行匹配,或者暴力else if进行配对判断,但这里我们可以灵活使用栈,但匹配到左括号时,让对应的右括号入栈,这样当匹配到右括号时只需要比较右括号跟栈顶的元素是否相等,这种写法大大减少了我们的代码量

var isValid = function(s) {
const stack = [];
for (let val of s) {
    console.log(stack)
    if (val === '(') stack.push(')');
    else if (val === '[') stack.push(']');
    else if (val === '{') stack.push('}');
    else if (stack.length === 0 || val !== stack.pop()) return false;
}
return stack.length === 0;
};

最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

跟上一道题很相似,但这里要返回最长的字串长度。最开始我的思路是设置两个栈,当匹配到左括号时不断入其中一个栈(常栈),当匹配到右括号时,常栈出栈并将出栈的元素push进另一个栈(临时栈),记录临时栈的长度,当常栈匹配的过程种发现无法匹配到有效括号时,临时栈清,重新添加并挑战最大长度。代码实现如下:

var longestValidParentheses = function(s) {
    let stk = [];
    let max = 0;
    let temp = [];
    s = s.split("");
    for (let i = 0; i < s.length; i++) {
        if (stk.length == 0 && s[i] == ")") {
            console.log(temp)
            temp = [];
            continue;
        }
        if (s[i] == "(") {
            stk.push(s[i]);
        }
        if (stk.length != 0 && s[i] == ")") {
            temp.push(stk[stk.length - 1]);
            temp.push(s[i]);
            stk.pop();
            if (temp.length > max) {
                max = temp.length;
            }
        }
    }
    return max;
}

但这种算法存在问题,先不考虑开辟两个栈的内存消耗,如果出现像"(()"这种"最长有效括号在无效括号中"的情况,这种方法会匹配失败。

正确的解决方法是,通过保存下标并与起始下标相减来获得最大长度,基本原则是匹配到左括号进栈,匹配到右括号时,先出栈,如果栈非空,就通过下标计算长度并挑战最大长度,如果栈空,说明之前已经匹配完一组合法的括号,且新push进来的这个是不合法的,所有可以将他push进栈的第一项作为新的初始项,这里要注意一下初始项,由于下标是从0开始的,而且可能出现上面这种栈空进右括号的情况,所有我们需要定义一个初始值(-1)作为下标的第一位,之后出现上面的情况,就通过让右括号下标进栈的方式重置这个下标

var longestValidParentheses = function(s) {
let stk = [];
let max = 0;
stk.push(-1);
for (let i = 0; i < s.length; i++) {
    if (s[i] == "(") {
        stk.push(i);
    } else {
        stk.pop();
        if (stk.length != 0) {
            max = Math.max(max, i - stk[stk.length - 1]);
        } else {
            stk.push(i)
        }
    }
}
return max;
};

二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

基础题,不做过多解释

  var inorderTraversal = function(root) {
    if (!root) { return [] }
    let res = [];
    allTree(root);

    function allTree(node) {
        if (node.left) {
            allTree(node.left);
        }
        res.push(node.val);
        if (node.right) {
            allTree(node.right);
        }
    }
    return res;
	};

二叉树遍历为链表

展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。 展开后的单链表应该与二叉树 先序遍历 顺序相同。

了解了二叉数前序遍历的话,这个题的思路还是很容易想到的,但要注意一些问题,一方面是这道题中涉及大量的节点修改,这种情况下,如果直接修改链表的话,我们需要定义大量的指针,而且程序也会比较臃肿,合理的方式是用数组去保存所有节点,然后按顺序处理成链表,这样更有利于我们的处理。另外,力扣的console对于链表来说是不完整的!只会打印一部分,导致我在这道题上花费了大量的时间debug。以后要注意一下。

var flatten = function(root) {
const list = [];
preorderTraversal(root, list);
const size = list.length;
for (let i = 1; i < size; i++) {
    const prev = list[i - 1],
        curr = list[i];
    prev.left = null;
    prev.right = curr;
}
};

const preorderTraversal = (root, list) => {
if (root != null) {
    list.push(root);
    preorderTraversal(root.left, list);
    preorderTraversal(root.right, list);
}
}

柱状图中的最大矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。

这道题的难度还是比较高的,需要发现一个规律:当新的柱子的高度小于栈顶的元素时,需要进行判断,获取到当前矩形的最大值并挑战总的最大值,第一次自己写代码的时候,忽略了可能在新元素加入后,柱状图中间出现最大块的情况

// 边界条件考虑不足
var largestRectangleArea = function(heights) {
let stk = [];
let max = 0;
heights.unshift(0);
heights.push(0);
for (let i = 0; i < heights.length; i++) {
    console.log(stk, max);
    if (stk.length == 0 || (heights[i] >= stk[stk.length - 1])) {
        stk.push(heights[i]);
    }
    if (heights[i] < stk[stk.length - 1]) {
        let k = 0;
        while (stk[stk.length - 1] > heights[i]) {
            stk.pop();
            k++;
        }
        max = k * heights[i - k] > max ? k * heights[i - k] : max;
    }
}
return max;
};

正确答案对于这种情况的解决方案是:在每次判断时,进行逐次循环判断,保证遍历到每一种情况

  const largestRectangleArea = (heights) => {
    let maxArea = 0
    const stack = []
    heights = [0, ...heights, 0]
    for (let i = 0; i < heights.length; i++) { //只有小于才循环,将相等的部分一并入栈
        while (heights[i] < heights[stack[stack.length - 1]]) { // 当前bar比栈顶bar矮
            const stackTopIndex = stack.pop() // 栈顶元素出栈,并保存栈顶bar的索引
            maxArea = Math.max( // 计算面积,并挑战最大面积
                maxArea, // 计算出栈的bar形成的长方形面积
                //这里中间逐次比较,防止出现某几项过高的边界条件
                heights[stackTopIndex] * (i - stack[stack.length - 1] - 1)
            )
        }
        stack.push(i) // 当前bar比栈顶bar高了,入栈
    }
    return maxArea
	}

小结

  • 处理链表时,如果出现需要大量移动链表元素的情况时,可以考虑使用循环链表(基本的位置顺序不变)或者使用数组暂时存放节点(链表的整体顺序会出现比较大的改变)来取代大量修改节点的操作,注意用数组暂时存放节点时,要将next置为null清空,重新组织时再重新连接否则容易出现节点循环。对于循环链表,也要注意拆开节点的方法是将拆分点的next置为null
  • 对链表的删除活修改注意从后往前进行,防止出现节点丢失的情况
  • 注意边界条件,当出现类似二分法的处理时,可以以小于和大于等于为一组,更便于处理
  • 使用栈时,push进栈的不一定是元素本身,元素的下标,元素对应的部分,甚至与其不相干的部分都可以,很多时候对入栈出栈的元素的合理选择能简化我们的思路和操作
  • 可以根据情况再栈的两边添加参照物,方便我们处理,这个参照物可以是动态更改的
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值