链表初识 链表:类似于数组,都是有序列表,线性结构。
不同点在于,链表中,数据单位的名称叫做“结点”,而结点和结点的分布,相对于数据的连续来说在内存中可以是离散的。
实现一个链表,通过嵌套对象的形式实现
1 2 3 4 5 6 7 8 9 const listInJS = { val : 1 , next : { val : 2 , next : null , }, };
1 2 3 4 5 6 function ListNode (val ) { this .val = val; this .next = null ; } const node = new ListNode (1 );console .log (node);
添加
1 2 3 4 const node2 = new ListNode (2 );node.next = node2; console .log (node);
插入
1 2 3 4 5 const node3 = new ListNode (3 );node3.next = node2; node.next = node3; console .log (node);
删除
1 2 3 node.next = node3.next ; console .log (node);
Map的妙用
真题描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。 示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
1 2 3 4 5 6 7 8 const timer = (callback ) => { console .time ("timer" ); console .log (callback ()); console .timeEnd ("timer" ); }; const nums = [2 , 7 , 11 , 15 , 1 , 8 ];const target = 9 ;
普通解法, 使用两层for循环,用nums[i]和nums[j]相加,若等于target, 则向result中push[i, j]
1 2 3 4 5 6 7 8 9 10 11 12 13 const twoSum1 = (nums, target ) => { const result = []; for (let i = 0 ; i < nums.length ; i++) { for (let j = i + 1 ; j < nums.length ; j++) { if (nums[i] + nums[j] === target) { result.push ([i, j]); } } } return result; }; timer (() => twoSum1 (nums, target));
从上面的解法来看,两层循环的时间复杂度是O(n^2),每次都要将第一次遍历的数与后面的所有数相加与target比较
巧用map解法空间换时间,将求和问题变成球差问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const twoSum2 = (nums, target ) => { const map = {}; const res = []; for (let i = 0 ; i < nums.length ; i++) { if (map[target - nums[i]] !== undefined ) { res.push ([map[target - nums[i]], i]); } map[nums[i]] = i; } return res; }; timer (() => twoSum2 (nums, target));
上述的方法将nums中的元素作为key,元素的下标作为value,这样就可以求出target-nums[i]的值,如果map中有这个值,则说明有两个元素的和等于target
使用es6的Map来做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const twoSum3 = (nums, target ) => { const map = new Map (); const res = []; for (let i = 0 ; i < nums.length ; i++) { if (map.get (target - nums[i]) !== undefined ) { res.push ([map.get (target - nums[i]), i]); } map.set (nums[i], i); } return res; }; timer (() => twoSum3 (nums, target));
以上使用map的解法,无论是js的对象还是es6的Map,都可以实现相同的功能,然后时间复杂度都是O(n) 留个疑问,如果你将代码运行一下就会发现使用es6的Map会比使用js对象所需的时间少个一半左右。why?
扁平数据转树 例:let arr = [ { id: 1, name: “部门1”, pid: 0 }, { id: 2, name: “部门2”, pid: 1 }, { id: 3, name: “部门3”, pid: 1 }, { id: 4, name: “部门4”, pid: 3 }, { id: 5, name: “部门5”, pid: 4 }, { id: 6, name: “部门6”, pid: 9 }, ];
1 2 3 4 5 6 7 8 let arr = [ { id : 1 , name : "部门1" , pid : 0 }, { id : 2 , name : "部门2" , pid : 1 }, { id : 3 , name : "部门3" , pid : 1 }, { id : 4 , name : "部门4" , pid : 3 }, { id : 5 , name : "部门5" , pid : 4 }, { id : 6 , name : "部门6" , pid : 9 }, ];
普通递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const flatToTree = (data, pid ) => { const result = []; const getChildren = (data, result, pid ) => { data.forEach ((item ) => { if (item.pid === pid) { const newItem = { ...item, children : [] }; result.push (newItem); getChildren (data, newItem.children , item.id ); } }); }; getChildren (data, result, pid); return result; }; timer (() => flatToTree (arr, 0 ));
使用Map的引用避免递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const flatToTree1 = (arr ) => { const map = {}; const res = []; arr.forEach ((item ) => { if (!map[item.id ]) { map[item.id ] = { children : [] }; } map[item.id ] = { ...item, children : map[item.id ]["children" ], }; if (item.pid === 0 ) { res.push (map[item.id ]); } else { if (!map[item.pid ]) { map[item.pid ] = { children : [], }; } map[item.pid ].children .push (map[item.id ]); } }); console .log (JSON .stringify (res)); return res; }; timer (() => flatToTree1 (arr));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function flatToTree2 (items ) { const result = []; const itemMap = {}; for (const item of items) { const id = item.id ; const pid = item.pid ; itemMap[id] = { ...item, children : [], }; if (pid === 0 ) { result.push (itemMap[id]); } else { if (!itemMap[pid]) { itemMap[pid] = { children : [], }; } itemMap[pid].children .push (itemMap[id]); } } console .log (JSON .stringify (res)); return result; } console .log (JSON .stringify (flatToTree2 (arr)));
双指针法合并有序数组
真题描述:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。 说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例: 输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
定义一个统计时间的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const timer = (callback ) => { console .time ("timer" ); console .log (callback ()); console .timeEnd ("timer" ); }; const nums1 = [1 , 2 , 3 , 0 , 0 , 0 ];const nums2 = [2 , 5 , 6 ];const m = 3 , n = 3 ; const merge = (nums1, nums2, m, n ) => { let i = m - 1 , j = n - 1 , k = m + n - 1 ; while (i >= 0 && j >= 0 ) { if (nums1[i] > nums2[j]) { nums1[k] = nums1[i]; i--; k--; } else { nums1[k] = nums2[j]; j--; k--; } } while (j >= 0 ) { nums1[k] = nums2[j]; k--; j--; } };
在js中的另辟蹊径
1 2 3 4 5 6 7 const merge2 = ( ) => { nums1.splice (m, n, ...nums2); nums1.sort ((a, b ) => a - b); }; timer (() => merge (nums1, nums2, m, n));console .log (nums1);
双指针法三数求和
真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 注意:答案中不可以包含重复的三元组。
示例: 给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
定义一个统计时间的函数
1 2 3 4 5 const timer = (callback ) => { console .time ("timer" ); console .log (callback ()); console .timeEnd ("timer" ); };
如果按照最传统的思路可能需要三层循环,时间复杂度为 O(n^3),肯定是不合适的。但是使用指针之后就会大大降低时间复杂度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const nums = [-1 , 0 , 1 , 2 , 2 , 2 , -1 , -4 ];const threeSum = (nums ) => { nums = nums.sort ((a, b ) => a - b); let result = []; for (let i = 0 ; i < nums.length - 2 ; i++) { let m = i + 1 , n = nums.length - 1 ; while (m < n) { if (nums[m] + nums[n] + nums[i] > 0 ) { n--; while (nums[n] === nums[n - 1 ]) { n--; } } else if (nums[m] + nums[n] + nums[i] < 0 ) { m++; while (nums[m] === nums[m + 1 ]) { m++; } } else { result.push ([nums[m], nums[n], nums[i]]); m++; n--; while (nums[n] === nums[n - 1 ]) { n--; } while (nums[m] === nums[m + 1 ]) { m++; } } } } return result; }; timer (() => threeSum (nums));
总结一下:遇到“有序”和“数组”这两个关键字,立刻把就要想到双指针法普通双指针走不通,立刻想对撞指针!
回文字符串 判断一个字符串是否是回文字符串;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const str = "yessey1" ;function isPalindrome1 (str ) { const reversedStr = str.split ("" ).reverse ().join ("" ); return reversedStr === str; } console .log (isPalindrome1 (str));function isPalindrome2 (str ) { const len = str.length ; for (let i = 0 ; i < len / 2 ; i++) { if (str[i] !== str[len - i - 1 ]) { return false ; } } return true ; } console .log (isPalindrome2 (str));
真题描述:给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 示例 1: 输入: “aba” 输出: True 示例 2: 输入: “abca” 输出: True 解释: 你可以删除c字符。 注意: 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const validPalindrome = (str ) => { const len = str.length ; let j = 0 , k = len - 1 ; while (j < k && str[j] === str[k]) { j++; k--; } if (isPalindrome (j + 1 , k)) { return true ; } if (isPalindrome (j, k - 1 )) { return true ; } return false ; function isPalindrome (start, end ) { while (start < end) { if (str[start] !== str[end]) return false ; start++; end--; } return true ; } }; console .log (validPalindrome (str));
字符串匹配问题
真题描述: 设计一个支持以下两种操作的数据结构: void addWord(word) bool search(word) search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z 。 . 可以表示任何一个字母。
示例: addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true
说明:
你可以假设所有单词都是由小写字母 a-z 组成的。
1 2 3 4 5 const timer = (callback ) => { console .time ("timer" ); console .log (callback ()); console .timeEnd ("timer" ); };
这个需要addWord和search两个功能,所以数据需要存在某个地方。 分析:最简单的办法就是数组,将字符串存进数组,search的时候如果没有”.”就直接用includes判断,如果有就new RegExp()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class WordDictionary_ { constructor ( ) { this .words = []; } addWord (word ) { if (!this .words .includes (word)) this .words .push (word); } search (word ) { if (!word.includes ("." )) { return this .words .includes (word); } const reg = new RegExp (word); return this .words .some ((item ) => { return reg.test (item); }); } }
这里为了降低查找时的复杂度,我们可以考虑以字符串的长度为 key,相同长度的字符串存在一个数组中,这样可以提高我们后续定位的效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class WordDictionary { constructor ( ) { this .words = {}; } addWord (word ) { if (this .words [word.length ]) { this .words [word.length ].push (word); } else { this .words [word.length ] = [word]; } } search (word ) { if (!this .words [word.length ]) { return false ; } const len = word.length ; if (!word.includes ("." )) { return this .words [len].includes (word); } const reg = new RegExp (word); return this .words [len].some ((item ) => { return reg.test (item); }); } } const word = new WordDictionary ();word.addWord ("bav" ); timer (() => word.search ("b.." ));
字符串与数字之间的转换问题
真题描述:请你来实现一个 atoi 函数,使其能将字符串转换成整数。 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。 当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。 该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。 注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。 在任何情况下,若函数不能进行有效的转换时,请返回 0。 说明: 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31, 2^31 − 1]。如果数值超过这个范围,请返回 INT_MAX (2^31 − 1) 或 INT_MIN (−2^31) 。 示例 1: 输入: “42” 输出: 42 示例 2: 输入: “ -42” 输出: -42 解释: 第一个非空白字符为 ‘-‘, 它是一个负号。 我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。 示例 3: 输入: “4193 with words” 输出: 4193 解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。 示例 4: 输入: “words and 987” 输出: 0 解释: 第一个非空字符是 ‘w’, 但它不是数字或正、负号。 因此无法执行有效的转换。 示例 5: 输入: “-91283472332” 输出: -2147483648 解释: 数字 “-91283472332” 超过 32 位有符号整数范围。因此返回 INT_MIN (−2^31) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const atoi = (str ) => { const reg = /\s*([-\+]?[0-9]*).*/ ; const group = str.match (reg); const max = Math .pow (2 , 31 ) - 1 ; const min = -max - 1 ; let res; if (group) { res = +group[1 ]; console .log (res); if (isNaN (res)) { res = 0 ; } } if (res > max) { return max; } else if (res < min) { return min; } return res; }; console .log (atoi ("-123" ));
这题需要对正则比较了解 首先,\s 这个符号,意味着空字符,它可以用来匹配回车、空格、换行等空白区域,这里,它用来被匹配空格。这个符号,跟在其它符号后面,意味着“前面这个符号可以出现0次或多次。\s ,这里的意思就是空格出现0次或多次,都可被匹配到。 接着 () 出现了。() 圈住的内容,就是我们要捕获起来额外存储的东西。 []中的匹配符之间是“或”的关系,也就是说只要能匹配上其中一个就行了。 这里[]中包括了-和+,-不必说匹配的是对应字符,这个+之所以加了一个斜杠符,是因为+本身是一个有特殊作用的正则匹配符,这里我们要让它回归+字符的本义,所以要用一个\来完成转义。 [0-9]*结合咱们前面铺陈的知识,这个就不难理解了,它的意思是 0-9 之间的整数,能匹配到0个或多个就算匹配成功。 最后的 .这个是任意字符的意思,.*用于字符串尾部匹配非数字的任意字符。我们看到.*是被排除捕获组之外的,所以说这个东西其实也不会被额外存储,它被“摘除”了。
链表的合并
真题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。 示例: 输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4
1 2 3 4 5 function ListNode (val, next ) { this .val = val; this .next = next || null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const l1 = new ListNode (1 , new ListNode (2 , new ListNode (4 )));const l2 = new ListNode (1 , new ListNode (3 , new ListNode (4 )));const mergeTwoList = (l1, l2 ) => { const head = new ListNode (); let cur = head; while (l1 && l2) { if (l1.val <= l2.val ) { cur.next = l1; l1 = l1.next ; } else { cur.next = l2; l2 = l2.next ; } cur = cur.next ; } cur.next = l1 !== null ? l1 : l2; return head.next ; }; console .log (JSON .stringify (mergeTwoList (l1, l2)));
链表的删除
真题描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 示例 1: 输入: 1->1->2 输出: 1->2 示例 2: 输入: 1->1->2->3->3 输出: 1->2->3
1 2 3 4 5 function ListNode (val, next ) { this .val = val; this .next = next || null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 const list = new ListNode (1 , new ListNode (1 , new ListNode (2 )));const deleteDuplicates = (list ) => { const cur = list; while (!cur && !cur.next ) { if (cur.val === cur.next .val ) { cur.next = cur.next .next ; } else { cur = cur.next ; } } return list; }; console .log (deleteDuplicates (list));
删除问题的延伸——dummy 结点
真题描述:给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。 示例 1: 输入: 1->2->3->3->4->4->5 输出: 1->2->5 示例 2: 输入: 1->1->1->2->3 输出: 2->3
我们先来分析一下这道题和上道题有什么异同哈:相同的地方比较明显,都是删除重复元素。 不同的地方在于,楼上我们删到没有重复元素就行了,可以留个“独苗”;但现在,题干要求我们只要一个元素发生了重复,就要把它彻底从链表中干掉,一个不留。 时我们就可以用一个 dummy 结点解决这个问题。 所谓 dummy 就是人为制造出来的第一个结点的前驱结点,这样链表中所有的结点都能确保有一个前驱结点,也就都能够用同样的逻辑来处理了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const list2 = new ListNode ( 1 , new ListNode ( 1 , new ListNode (2 , new ListNode (3 , new ListNode (3 , new ListNode (3 )))) ) ); const deleteAllDuplicates = (list ) => { let dummy = new ListNode (); dummy.next = list; let cur = dummy; while (cur.next ?.next ) { if (cur.next .val === cur.next .next .val ) { let val = cur.next .val ; while (cur.next ?.val === val) { cur.next = cur.next .next ; } } else { cur = cur.next ; } } return dummy.next ; }; console .log (deleteAllDuplicates (list2));
快慢指针 链表题目中,有一类会涉及到反复的遍历。 往往会涉及相对复杂的链表操作,比如反转、指定位置的删除等等。
快慢指针——删除链表的倒数第 N 个结点 真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例: 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个结点后,链表变为 1->2->3->5. 说明: 给定的 n 保证是有效的。
像上面说的,这题是属于指定位置的删除,这个时候需要两次遍历,如上题,删除倒数第n个就是删除正数length+1-n个 这个时候就需要一次遍历拿到链表长度。
1 2 3 4 5 function ListNode (val, next ) { this .val = val; this .next = next || null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const list = new ListNode (1 , new ListNode (2 , new ListNode (3 , new ListNode (4 ))));const removeNthFromEnd = (list, n ) => { const dummy = new ListNode (); dummy.next = list; let cur = dummy; let count = 0 ; while (cur?.next ) { count += 1 ; cur = cur.next ; } cur = dummy; let current = 0 ; while (cur?.next ) { current += 1 ; if (current === count + 1 - n) { cur.next = cur.next .next ; } else { cur = cur.next ; } } return dummy.next ; };
不过这种超过一次的遍历必然需要引起我们的注意,我们应该主动去思考,“如果一次遍历来解决这个问题,我可以怎么做?”,这时候,就要请双指针法来帮忙了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const removeNthFromEnd2 = (list, n ) => { const dummy = new ListNode (); dummy.next = list; let slow = dummy; let fast = dummy; while (fast?.next ) { if (n > 0 ) { fast = fast.next ; n--; } else { fast = fast.next ; slow = slow.next ; } } slow.next = slow.next .next ; return dummy; }; console .log (JSON .stringify (removeNthFromEnd2 (list, 2 )));
多指针实现链表的反转 链表的反转
真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
处理链表的本质,是处理链表结点之间的指针关系。
1 2 3 4 5 function ListNode (val, next ) { this .val = val; this .next = next || null ; }
遍历链表的时候让当前节点next指向上一个节点即可,需要注意next指向改变之后会导致丢失后续的遍历,需要一个next节点用来保存断开的后续节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const list1 = new ListNode ( 1 , new ListNode (2 , new ListNode (3 , new ListNode (4 , new ListNode (5 )))) ); const reverseList = (list ) => { let cur = list; let pre = null ; while (cur) { let next = cur.next ; cur.next = pre; pre = cur; cur = next; } return pre; }; console .log (JSON .stringify (reverseList (list1)));
局部反转一个链表
真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。 说明: 1 ≤ m ≤ n ≤ 链表长度。 示例: 输入: 1->2->3->4->5->NULL, m = 2, n = 4 输出: 1->4->3->2->5->NULL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const list2 = new ListNode ( 1 , new ListNode (2 , new ListNode (3 , new ListNode (4 , new ListNode (5 )))) ); const reverseBetween = (list, m, n ) => { let pre, cur, leftHead; const dummy = new ListNode (); dummy.next = list; leftHead = dummy; for (let i = 0 ; i < m - 1 ; i++) { leftHead = leftHead.next ; } let start = leftHead.next ; pre = start; cur = pre.next ; for (let i = m; i < n; i++) { let next = cur.next ; cur.next = pre; pre = cur; cur = next; } leftHead.next = pre; start.next = cur; return dummy.next ; }; console .log (JSON .stringify (reverseBetween (list2, 2 , 4 )));
环形链表
真题描述:给定一个链表,判断链表中是否有环。 示例 1: 输入:[3,2,0,4] 输出:true 解释:链表中存在一个环
1 2 3 4 5 function ListNode (val, next ) { this .val = val; this .next = next || null ; }
1 2 3 const list = new ListNode (1 , new ListNode (2 , new ListNode (3 )));list.next .next .next = list.next ;
我们在遍历链表的时候添加一个flag,向后遍历的时候如果存在node.flag===true 就说明链表是环形
1 2 3 4 5 6 7 8 9 10 11 12 const hasCycle = (list ) => { while (list?.next ) { if (list?.flag ) { return true ; } else { list.flag = true ; list = list.next ; } } return false ; };
环形链表衍生问题——定位环的起点 这个问题和上面的其实基本一样,在第一次判断到flag为true的时候将当前节点返回即可
1 2 3 4 5 6 7 8 9 10 11 12 const detectCycle = (list ) => { while (list?.next ) { if (list?.flag ) { return list; } else { list.flag = true ; list = list.next ; } } return null ; }; console .log (detectCycle (list));
这道题还有一个公认的比较经典的思路,就是用快慢指针来做: 定义慢指针 slow,快指针 fast。两者齐头并进, slow 一次走一步、fast 一次 走两步。这样如果它们是在一个有环的链表里移动,一定有相遇的时刻。
栈的应用 括号问题
题目描述:给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。 示例 1: 输入: “()” 输出: true 示例 2: 输入: “()[]{}” 输出: true 示例 3: 输入: “(]” 输出: false 示例 4: 输入: “([)]” 输出: false 示例 5: 输入: “{[]}” 输出: true
括号问题在面试中出现频率非常高, 这类题目我们一般首选用栈来做。 为什么可以用栈做?大家想想,括号成立意味着什么?意味着对称性。 巧了,根据栈的后进先出原则,一组数据的入栈和出栈顺序刚好是对称的。比如说1、2、3、4、5、6按顺序入栈,其对应的出栈序列就是 6、5、4、3、2、1 对称关系一目了然。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const isValid = (str ) => { const stack = []; const leftBrackets = ["{" , "[" , "(" ]; const leftToRight = { "(" : ")" , "[" : "]" , "{" : "}" , }; for (let i = 0 ; i < str.length ; i++) { if (leftBrackets.includes (str[i])) { stack.push (leftToRight[str[i]]); } else { if (stack.pop () !== str[i]) return false ; } } return true ; };
每日温度问题
题目描述: 根据每日气温列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。 例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
思路:尝试去维持一个递减栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const dailyTemperatures = (t ) => { const stack = []; const res = new Array (t.length ).fill (0 ); for (let i = 0 ; i < t.length ; i++) { while (stack.length && t[i] > t[stack[stack.length - 1 ]]) { const top = stack.pop (); res[top] = i - top; } stack.push (i); } return res; }; console .log (dailyTemperatures ([73 , 72 , 75 , 71 , 69 , 72 , 76 , 73 ]));
最小栈问题
题目描述:设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 push(x) —— 将元素 x 推入栈中。 pop() —— 删除栈顶的元素。 top() —— 获取栈顶元素。 getMin() —— 检索栈中的最小元素。 示例: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); –> 返回 -3. minStack.pop(); minStack.top(); –> 返回 0. minStack.getMin(); –> 返回 -2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class MinStack { constructor ( ) { this .stack = []; } push (val ) { this .stack .push (val); } pop ( ) { return this .stack .pop (); } top ( ) { return this .stack [this .stack .length - 1 ]; } getMin ( ) { return Math .min (...this .stack ); } } const minStack = new MinStack ();minStack.push (-2 ); minStack.push (0 ); minStack.push (-3 ); console .log (minStack.getMin ());
从栈到队列的世界 如何用栈实现一个队列?
题目描述:使用栈实现队列的下列操作: push(x) – 将一个元素放入队列的尾部。 pop() – 从队列首部移除元素。 peek() – 返回队列首部的元素。 empty() – 返回队列是否为空。 示例: MyQueue queue = new MyQueue(); queue.push(1); queue.push(2); queue.peek(); // 返回 1 queue.pop(); // 返回 1 queue.empty(); // 返回 false 说明: 你只能使用标准的栈操作 – 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。
由题意我们无法使用push和shift来简单实现队列而是需要使用栈结构,在js中没有栈结构,我们可以使用list代替,使用push pop length 来实现队列
我们知道队列是先进先出和栈后进先出是相反的操作,我们只需要使用两个栈,stack1和stack2,在实现队列出列之前将stack1出栈存放至stack2,stack2的pop操作就相当于队列的pop操作了 peek也是同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 ) while (this .stack1 ?.length ) { this .stack2 .push (this .stack1 .pop ()); } return this .stack2 .pop (); }; MyQueue .prototype .peek = function ( ) { if (this .stack2 .length === 0 ) while (this .stack1 ?.length ) { this .stack2 .push (this .stack1 .pop ()); } return this .stack2 [this .stack2 .length - 1 ]; }; MyQueue .prototype .empty = function ( ) { return !this .stack1 .length && !this .stack2 .length ; }; const myQueue = new MyQueue ();console .log (myQueue.empty ());
认识双端队列
题目描述:给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。 示例: 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7] 解释: 滑动窗口的位置 [1 3 -1] -3 5 3 6 7 1 [3 -1 -3] 5 3 6 7 1 3 [-1 -3 5] 3 6 7 1 3 -1 [-3 5 3] 6 7 1 3 -1 -3 [5 3 6] 7 1 3 -1 -3 5 [3 6 7] 最大值分别对应: 3 3 5 5 6 7 提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
通常可以使用双指针解决,但是使用双端队列可以降低时间复杂度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const maxSlidingWindow = function (nums, k ) { let left = 0 ; let right = k - 1 ; const res = []; while (right < nums.length ) { let max = nums[left]; for (let i = left; i < left + k; i++) { max = Math .max (max, nums[i]); } res.push (max); left++; right++; } return res; };
O(kn)—>O(n) 使用双端队列实现一个单调递减的队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const maxSlidingWindow2 = function (nums, k ) { const len = nums.length ; let res = []; let queue = []; for (let i = 0 ; i < len; i++) { while (nums[i] >= nums[queue[queue.length - 1 ]]) { queue.pop (); } queue.push (i); if (i >= k - 1 ) { while (queue[0 ] <= i - k) { queue.shift (); } res.push (nums[queue[0 ]]); } } return res; };
应用
题目描述:给定一个没有重复数字的序列,返回其所有可能的全排列。 示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const permute = (nums ) => { const res = []; let cur = []; const visited = {}; function dfs (depth ) { if (depth === nums.length ) { res.push ([...cur]); return ; } for (let i = 0 ; i < nums.length ; i++) { if (!visited[nums[i]]) { visited[nums[i]] = 1 ; cur.push (nums[i]); dfs (depth + 1 ); cur.pop (); visited[nums[i]] = 0 ; } } } dfs (0 ); return res; }; console .log (permute ([1 , 2 , 3 ]));
题目描述:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const subsets = function (nums ) { const res = []; const len = nums.length ; const cur = []; dfs (0 ); function dfs (index ) { res.push (cur.slice ()); for (let i = index; i < len; i++) { cur.push (nums[i]); dfs (i + 1 ); cur.pop (); } } return res; }; console .log (subsets ([1 , 2 , 3 ]));
题目描述:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const combine = function (n, k ) { const res = []; const cur = []; dfs (1 ); function dfs (index ) { if (cur?.length === k) { res.push ([...cur]); return ; } for (let i = index; i <= n; i++) { cur.push (i); dfs (i + 1 ); cur.pop (); } } return res; }; console .log (combine (4 , 2 ));
什么时候用递归回溯思想 看两个特征: 题目中暗示了一个或多个解,并且要求我们详尽地列举出每一个解的内容时,一定要想到 DFS、想到递归回溯。 题目经分析后,可以转化为树形逻辑模型求解。 一个模型——树形逻辑模型;两个要点——递归式和递归边界。 树形逻辑模型的构建,关键在于找“坑位”,一个坑位就对应树中的一层,每一层的处理逻辑往往是一样的,这个逻辑就是递归式的内容。至于递归边界,要么在题目中约束得非常清楚、要么默认为“坑位”数量的边界。
function xxx(入参) { 前期的变量定义、缓存等准备工作 // 定义路径栈 const path = [] // 进入 dfs dfs(起点) // 定义 dfs dfs(递归参数) { if(到达了递归边界) { 结合题意处理边界逻辑,往往和 path 内容有关 return } // 注意这里也可能不是 for,视题意决定 for(遍历坑位的可选值) { path.push(当前选中值) 处理坑位本身的相关逻辑 path.pop() } } }
没有思路的时候对照着这个模板想一想说不定就有了呢
二叉树遍历 先(前)序遍历迭代实现
题目描述:给定一个二叉树,返回它的前序(先序)遍历序列。 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,2,3] 进阶: 递归算法很简单,你可以通过迭代算法完成吗?
1 2 3 4 5 function TreeNode (val, left, right ) { this .val = val === undefined ? 0 : val; this .left = left === undefined ? null : left; this .right = right === undefined ? null : right; }
不使用递归 使用迭代算法
前序遍历的规则是,先遍历根结点、然后遍历左孩子、最后遍历右孩子——这正是我们所期望的出栈序列。按道理,入栈序列和出栈序列相反,我们似乎应该按照 右->左->根 这样的顺序将结点入栈。不过需要注意的是,我们遍历的起点就是根结点,难道我们要假装没看到这个根结点、一鼓作气找到最右侧结点之后才开始进行入栈操作吗?答案当然是否定的,我们的出入栈顺序应该是这样的: 1.将根结点入栈 2.取出栈顶结点,将结点值 push 进结果数组 3.若栈顶结点有右孩子,则将右孩子入栈 4.若栈顶结点有左孩子,则将左孩子入栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var preorderTraversal = function (root ) { const stack = [root]; const res = []; if (!root) { return res; } while (stack?.length ) { const cur = stack.pop (); res.push (cur.val ); if (cur.right ) { stack.push (cur.right ); } if (cur.left ) { stack.push (cur.left ); } } return res; };
异曲同工的后序遍历迭代实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const postorderTraversal = function (root ) { const res = []; if (!root) { return res; } const stack = [root]; while (stack.length ) { const cur = stack.pop (); res.unshift (cur.val ); if (cur.left ) { stack.push (cur.left ); } if (cur.right ) { stack.push (cur.right ); } } return res; };
思路清奇的中序遍历迭代实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const inorderTraversal = function (root ) { const res = []; const stack = []; let cur = root; while (cur || stack.length ) { while (cur) { stack.push (cur); cur = cur.left ; } cur = stack.pop (); res.push (cur.val ); cur = cur.right ; } return res; };
层序遍历的衍生问题
题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 示例: 二叉树:[3,9,20,null,null,15,7], 3 / 9 20 / 15 7 返回其层次遍历结果: [ [3], [9,20], [15,7] ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const levelOrder = function (root ) { const res = []; if (!root) { return res; } const queue = []; queue.push (root); while (queue.length ) { const level = []; const len = queue.length ; for (let i = 0 ; i < len; i++) { const top = queue.shift (); level.push (top.val ); if (top.left ) { queue.push (top.left ); } if (top.right ) { queue.push (top.right ); } } res.push (level); } return res; };
翻转二叉树
题目描述:翻转一棵二叉树。 示例: 输入:
4
/ 2 7 / \ / 1 3 6 9 输出: 4 / 7 2 / \ / 9 6 3 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const invertTree = function (root ) { if (!root) { return root; } let right = invertTree (root.right ); let left = invertTree (root.left ); root.left = right; root.right = left; return root; };
二叉树搜索专题 二叉搜索树(Binary Search Tree)简称 BST,是二叉树的一种特殊形式。它有很多别名,比如排序二叉树、二叉查找树等等。
什么是二叉搜索树?
是一棵空树
是一棵由根结点、左子树、右子树组成的树,同时左子树和右子树都是二叉搜索树, 且左子树上所有结点的数据域都小于等于根结点的数据域,右子树上所有结点的数据域都大于等于根结点的数据域 即 左孩子 <= 根结点 <= 右孩子
关于二叉搜索树,大家需要掌握以下高频操作:
查找数据域为某一特定值的结点
插入新结点
删除指定结点
1 2 3 4 5 function TreeNode (val, left, right ) { this .val = val === undefined ? 0 : val; this .left = left === undefined ? null : left; this .right = right === undefined ? null : right; }
查找数据域为某一特定值的结点
1 2 3 4 5 6 7 8 9 10 const search = (root, n ) => { if (!root) return ; if (root.val === n) { console .log ("目标结点是:" , root); } else if (root.val > n) { search (root.left , n); } else if (root.val < n) { search (root.right , n); } };
插入新结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function insertIntoBST (root, n ) { if (!root) { root = new TreeNode (n); return root; } if (root.val > n) { root.left = insertIntoBST (root.left , n); } else { root.right = insertIntoBST (root.right , n); } return root; }
删除指定结点 结点删除后只需要继续保持二叉查找树的特性即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const deleteNode = (root, n ) => { if (!root) return root; if (root.val === n) { if (!root.left && !root.right ) { root = null ; } else if (root.left ) { const maxLeft = findMax (root.left ); root.val = maxLeft.val ; root.left = deleteNode (root.left , maxLeft.val ); } else { const minRight = findMin (root.right ); root.val = minRight.val ; root.right = deleteNode (root.right , minRight.val ); } } else if (root.val > n) { root.left = deleteNode (root.left , n); } else { root.right = deleteNode (root.right , n); } return root; function findMax (root ) { while (root.right ) { root = root.right ; } return root; } function findMin (root ) { while (root.left ) { root = root.left ; } return root; } };
二叉搜索树真题
题目描述:给定一个二叉树,判断其是否是一个有效的二叉搜索树。 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var isValidBST = function (root ) { function dfs (root, minValue, maxValue ) { if (!root) { return true ; } if (root.val <= minValue || root.val >= maxValue) return false ; return ( dfs (root.left , minValue, root.val ) && dfs (root.right , root.val , maxValue) ); } return dfs (root, -Infinity , Infinity ); };
题目描述:将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。 示例: 给定有序数组: [-10,-3,0,5,9], 一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
1 2 3 4 5 6 7 8 9 10 11 12 var sortedArrayToBST = function (nums ) { if (!nums.length ) return null ; function buildBST (left, right ) { if (left > right) return null ; const mid = Math .floor (left + (right - left) / 2 ); const cur = new TreeNode (nums[mid]); cur.left = buildBST (left, mid - 1 ); cur.right = buildBST (mid + 1 , right); return cur; } return buildBST (0 , nums.length - 1 ); };