数据结构
数组
- JavaScript并不限制数组的数据类型,因此数组可以存放任意类型的数据。
- 当一个数组存放相同类型的数据时,数组在内存空间上是连续存储的。
- JavaScript提供直接操作原数组来删除数组元素的方法,例如栈方法
pop()、队列方法shift()、splice()方法等。 - JavaScript中数组的长度是动态变化的,即新增数据会自动增加。也可以通过减少数组长度来丢弃不需要的数据。
二分法
- 面对有序数组的查找元素问题,通常可以考虑二分法。
- 二分法的基本思想:把数组一分为二,把目标值和中间的那个元素进行比较,再不断缩小目标值的存在区间,最终当mid值等于目标值时,返回mid元素的下标。
- 运用二分法的关键在于确定循环区间的边界:循环区间到底是左闭右开
[left, right),还是左闭右闭[left, right]?不同的边界情况下,对于循环结束、区间变化的处理都是不一样的。一旦确定好循环区间边界,接下来的逻辑都要跟着这个区间边界走。 - 确定mid值时,仅仅写
(left + right) / 2可能会出现数字过大溢出的情况(比如left和right都大于1/2的MAX_VALUE时,数字会溢出),可以采取更优化的写法left + (right - left)/2。
双指针法
双指针可以通过一个for循环来完成两个for循环遍历数组的工作,降低时间复杂度。
快慢双指针
- 面对一些数组的筛选问题时(比如删除重复项),可以考虑快慢双指针法。
- 快指针和慢指针可以理解为一个排雷的过程,快指针在前面排雷,慢指针负责守护安全区。
首尾双指针
- 面对一些数组的排序问题时,可以考虑首尾双指针法。
- 利用首尾元素的比较进行排序。
滑动窗口
-
滑动窗口法的本质还是双指针法,但是滑动窗口得到的是数组的一个连续区间。
-
滑动窗口法的基本思想是根据某个条件来不断改变数组的子序列,就像一个在滑动的窗口一样。
-
使用滑动窗口法的关键是要弄清楚:
-
什么时候要扩大窗口(改变右指针)?
-
什么时候要缩小窗口(改变左指针)?
-
子序列改变时是否还要进行什么操作(根据需求而定)?
模拟行为
- 给一个数组进行模拟成别的形式(例如矩阵),或者给出模拟后的形式,要求转化回数组。
- 该类题目没有什么算法,最关键是要确定好边界。例如螺旋矩阵题目,就可以通过不断缩小上下左右四条边界来框定数组和矩阵的关系。边界关系一旦梳理清楚,代码就水到渠成了。
链表
-
链表是一种通过指针把各个节点串联在一起的线性数据结构,链表的每个节点包括数据域和指针域,指针域存放着指向下一个节点的指针(
next)。 -
链表的节点在内存中并不是连续存储的,只是链表能通过指针把它们串联起来。
也正因为如此,对数据进行新增和删除的操作,和数组相比(时间复杂度为O(n)),链表的新增和删除效率更高,只要修改指针的指向即可完成(时间复杂度为O(1))。
与之相对的,链表的查询效率就不及数组了。
- 链表有单链表和双链表,其中双链表的节点指针域不仅存放
next,还存放了指向前一个结点的指针(prev)。
虚拟头节点
- 涉及对链表的节点进行操作的问题(比如移除链表元素、删除倒数第n个节点、两两交换节点等等),通常要定义一个虚拟头节点。
- 虚拟头节点的作用是把头节点看成一个普通节点,免除了额外考虑头节点的情况,更便于我们对节点进行操作。
双指针法
- 链表中的双指针法通常指的是快慢指针法,涉及需要对链表进行遍历再解决的问题(比如反转链表、求链表的相交节点、环形链表问题等等),都可以考虑双指针。
- 对于环形链表求环入口的问题,要考虑清楚两个指针的所走路程之间的关系、以及所走路程和所求未知量的关系。
字符串
JavaScript中的字符串是不支持原地修改的,可以用[]来对字符串进行类似数组的访问,但是不能直接修改字符串,因此如果要对字符串中的字符进行操作,需要先用split("")或Array.from()得到字符串的数组形式。
常用库函数
string.replace方法
语法:string.replace(regexp/substr, replacement);
利用正则表达式来进行替换,注意不是原地替换的,而是返回了一个新字符串。
string.slice方法和string.subString方法
语法:string.slice(start, end);
都是用于提取字符串中包括start但不包括end的子字符串,如果只传入start,则默认end为string.length。
slice方法的参数可以接收负数,表示倒数第几个字符;subString方法只能接收正整数。
subString方法的传入参数,若start < end,则会默认交换二者。
Array.reverse()
反转数组元素。
操作字符串(反转、替换)
双指针法
- 需要处理字符串的某个区间子串时(比如反转字符串、按规律反转子串、替换子串等问题),都可以考虑双指针法。
- 使用双指针法解决字符串(或数组)的替换问题时,可以先扩容,再由后往前进行替换,如果从前往后替换,修改元素时需要再额外移动元素。
反转逻辑
- 对于字符串的反转,除了使用
reverse方法、临时变量法以外,还可以使用数组的解构赋值法。
while(++left < --right){
[s[left], s[right]] = [s[right], s[left]];
}
-
当需要按规律反转子串时,可以利用
for循环的表达式,让循环变量按照一定的规律改变。 -
反转题目的逻辑思路还包括:先局部反转再整体反转或先整体反转再局部反转。
字符串匹配(KMP算法)
KMP的作用
当遇到字符串匹配问题时,KMP可以帮助我们避免在匹配不成功时从头进行匹配,因为KMP算法会借助next数组,利用之前已经匹配成功的文本信息,跳转到相应位置进行匹配。
KMP是怎么做到的
前置概念
- 文本串:母串
模式串:子串,和文本串的某一部分进行匹配
- 前缀:从第一个字符开头的连续子串,不包括最后一个字符(包前不包后)
后缀:以最后一个字符结尾的连续子串,不包括第一个字符(包后不包前)
注意在循环过程中,前缀和后缀是随着循环变量的指针位置在不断变化的。
例如字符串 “
aabcdde” ,当指针走到c时,前缀是aab,后缀是abc。
- 最长相等前后缀:指针走到某个位置时,相等的前缀和后缀能达到的最长长度。
例如字符串 “
aabcaae” ,当指针走到c时,后缀可以是c、bc、abc,前缀可以是a、aa、aab,最长相等前后缀长度为0.当指针走到第四个a时,前缀和后缀都可以是
aa,此时最长相等前后缀长度为2.
-
前缀表:记录指针走到每一个字符时的最长相等前后缀数值。
-
next数组:可以是前缀表或者前缀表的变形(前缀表统一减一或前缀表向右移一位)
具体原理
因为前缀表记录的是指针走到每一个字符时的最长相等前后缀数值,所以如果这个数值大于0,那么截止到指针当前位置的字符串必定是相同子串 | 无法匹配字符 | 相同子串的形式,如果这个数值等于0,则说明指针当前位置的字符处于无法匹配字符这个区间里。
所以当我们遇到不匹配的字符时,既然当前字符已经匹配失败,就要看它的前一位的最长相等前后缀数值。
假设这个数值等于2,那么说明无法匹配字符区间是从模式串的第三个字符开始的,因此只要跳转到这个位置继续匹配即可。
代码实现KMP算法
构建next数组
- 从原理中可以知道KMP算法借助的实际上是前缀表,而next数组就是前缀表或者前缀表的变形,next数组的形式并不影响KMP的原理,只是影响KMP的代码实现。
比如,当next数组就是前缀表时,匹配失败时要看前一位的最长相等前后缀数值;
当next数组是前缀表向右移一位时(初始无法匹配字符的next数组值都会被赋值为-1),匹配失败时就看当前位的最长相等前后缀数值。
- 接下来的实现方法采取前缀表右移的next数组形式
/*
1. 初始化:
i:循环变量,遍历模式字符串,代表最长相等前后缀的后缀末尾
j:最长相等前后缀的前缀末尾,同时代表最长相等前后缀的长度(因为前缀末尾之前的都是匹配成功的)
不同的构建方式中,比较和跳转的方式也不同
若构建next[0]为-1的数组(前缀表向右移一位),遇到不匹配时j要直接跳转到next[j],则j初始值为-1, 比较s[i]和s[j+1]
若直接以前缀表构建数组,则next[0]为0,遇到不匹配时j跳转到next[j-1],则j初始值为0, 比较s[i]和s[j]
但是i的初始值都是1
此处以第一种方式为例,比较s[i]和s[j+1]来构建
2. 处理前后缀不相同的情况
j要向前回退(注意回退是一个循环过程),回退跳转规则见上
3. 处理前后缀相同的情况
前后缀匹配,把j赋给next[i]。注意这一步对j进行操作以后,会影响比较量,所以要放在处理不相同的情况之后
*/
function getNext(s) {
const next = [];
let j = -1;
next[0] = j;
for(let i = 1;i < s.length;i++){
while(j >= 0 && s[i] !== s[j+1]){
j = next[j];
}
if(s[i] === s[j+1]){
j++;
}
next[i] = j;
}
return next;
}
利用next数组进行匹配
- 初始化:
i:文本串的循环变量,注意这里和getNext()处的区别,next[0]是初始化时就确定的所以i从1开始比较,但是对于文本串要从0开始
j:和构建next数组里的j意义相同,因此初始化和跳转方式也应该和构建next数组时一致
-
处理前后缀不相同和相同的情况,逻辑和
getNext()相同 -
当j到了模式串的末尾位置时,匹配结束,返回模式串在文本串中的开头位置,由于i是最长相等前后缀的后缀末尾,j是最长相等前后缀的长度,所以返回
i - j即可
哈希表
- 哈希表是一种根据关键码的值(Key value)来进行访问的数据结构。
- 哈希表一般用于解决快速查找集合中的元素的问题,但是要注意哈希表常常是牺牲空间来换取时间。
- 常见的哈希表有数组、Set、Map。
数组
- 当存储连续数值时,浏览器引擎会对数组的内存进行优化,此时数组的查找效率是高于Map的,因此如果所存储数据为连续值,要优先考虑数组。
- 当查找范围的长度固定时(比如26个字母),也可以考虑数组。
- 比如383 赎金信 和 242 有效的字母异位词 中,查找范围是字母表(固定的26个字母),在数组中的表现形式是连续的ASII值,此时使用数组的查找效率更高。
Set
- Set具有去重性的特点,因此遇到需要排除重复元素的题目,可以考虑Set。
- 比如求交集、并集、检查是否出现循环等等。
Map
-
Map里的值以键-值对的形式存储,因此如果题目对返回值有其他要求(比如要得到数组中可行结果的下标),需要我们额外存储值时,可以考虑Map。
-
JavaScript中,同样空间的条件下,Map的空间利用率高于数组。
-
几数之和——哈希法和双指针法
- 两数之和:求解
nums数组中是否存在两个数,其和为target值,返回这两个数的下标。
由于要返回下标,因此使用哈希法快速查找nums中是否存在target - nums[i]更得当,如果要求返回是否存在,可以使用双指针法。
- 三数、四数之和:要求返回一个不重复的三元组/四元组。
此时用哈希法会使得去重非常困难,可以考虑双指针法。
双指针法:数组排序,使用循环变量来固定一个值(如果是三数之和,则固定nums[i],四数之和则嵌套两层for循环,固定nums[i] + nums[k]),移动left和right指针,找到当前固定值的所有可能组合。