算法的目标之一是:只要达到最优的时间复杂度就可以了(在此基础上再进一步优化代码的实现)。
举例说明:
// js 中定义一个二维数组
let arr = new Array(3).fill(0).map(() => new Array(4));
console.log(arr);
打印结果:
可见:二维数组在内存中并不是连续的地址空间,而是四条连续的地址空间组成。
双指针的宗旨是:用空间换时间。
跟具双指针的使用场景与特点,我将双指针分为:
题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例:
// 示例 1:
// 输入
nums = [-1,0,3,5,9,12], target = 9
// 输出: 4
// 解释: 9 出现在 nums 中并且下标为 4
// 示例 2:
// 输入
nums = [-1,0,3,5,9,12], target = 2
// 输出: -1
// 解释: 2 不存在 nums 中因此返回 -1
【解答】:
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
const search = (nums, target) => {
let l = 0
let r = nums.length - 1
while(l <= r) {
let m = Math.floor((r - l) / 2) + l
if (nums[m] > target) {
r = m - 1
} else if (nums[m] < target) {
l = m + 1
} else {
return m
}
}
return -1
};
总结如下。
二分查找的使用场景:在无重复元素的有序数组中查找某个元素。
二分法的实现方式取决于区间的定义。区间的定义有两种方式:
right = middle - 1
;if (nums[middle] < target) 则 left = middle + 1
。right = middle
;if (nums[middle] < target) 则 left = middle + 1
。二分法之所以容易写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在 while 寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
题目:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例:
// 示例 1:
// 输入:
nums = [3,2,2,3], val = 3
// 输出:2, nums = [2,2]
// 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
// 示例 2:
// 输入:
nums = [0,1,2,2,3,0,4,2], val = 2
// 输出:5, nums = [0,1,4,0,3]
// 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
【分析】:
这个题目暴力的解法就是两层 for 循环,不过时间复杂度是 O(n^2)。推荐使用 双指针法 来解这种题。
定义本题的快慢指针:
通过快指针遍历原数组中所有的数据,在原数组上移除所有等于目标值的数据:
【解题】:
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
*/
var removeElement = function(nums, val) {
let s = 0; // 指向的是原数组过滤时的存储地址
let f = 0; // 指向的是原数组里所有的数据
while(f < nums.length) {
if (nums[f] !== val) {
nums[s] = nums[f]; // 左移
s++;
}
f++;
}
// 注意:这里要返回的是 i
return i;
};
总结如下。
双指针法(又叫快慢指针法): 通过一个快指针和慢指针在一个 for 循环下完成两个 for 循环的工作——空间换时间。
平行双指针:指方向相同的双指针。除此之外,还有相向双指针(下面会讲)。常用于数组和链表的查询。
题目:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例:
// 示例 1:
// 输入:
nums = [-4,-1,0,3,10]
// 输出:[0,1,9,16,100]
// 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]
// 示例 2:
// 输入:
nums = [-7,-3,2,3,11]
// 输出:[4,9,9,49,121]
【解答】:
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function(nums) {
let result = []
let i = 0
let j = nums.length - 1
while(i <= j) {
let powNum = 0
if (nums[i] * nums[i] > nums[j] * nums[j]) {
powNum = Math.pow(nums[i++], 2)
} else {
powNum = Math.pow(nums[j--], 2)
}
result.unshift(powNum)
}
return result
};
总结如下:
相向双指针:指方向相对的双指针,最终会在某一处相交。常用于数组的排序。
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例:
// 示例 1:
// 输入:
target = 7, nums = [2,3,1,2,4,3]
// 输出:2
// 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
// 示例 2:
// 输入:
target = 4, nums = [1,4,4]
// 输出:1
// 示例 3:
// 输入:
target = 11, nums = [1,1,1,1,1,1,1,1]
// 输出:0
【解答】:
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
// 滑动窗口双指针
var minSubArrayLen = function(target, nums) {
let i = 0 // 起始位置
let j = 0 // 结束位置
let sum = 0
let res = nums.length + 1 // 子数组最大不会超过自身
while (j < nums.length) {
sum += nums[j++]
while (sum >= target) {
res = res > j - i ? j - i : res
sum -= nums[i++]
}
}
return res > nums.length ? 0 : res
}
总结如下。
滑动窗口就是:不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。也可以理解为双指针法的一种。
滑动窗口灵魂三问:
对于上述案例来讲,滑动窗口的精髓:根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将 O(n^2)的暴力解法降为 O(n)。
另外,徐阿哟特别注意的是:不要以为:for 里放一个 while,或者两个 while 嵌套了,它们的时间复杂度就一定是 O(n^2) 。而是要具体情况具体分析,主要是看每一个元素被操作的次数。
上述代码中,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是 O(n)。
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
// 输入:n = 3
// 输出:[[1,2,3],[8,9,4],[7,6,5]]
【分析】:
本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。
这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就很容易出错了。
模拟顺时针画矩阵的过程:
由外向内一圈一圈这么画下去。
由上图可知:用每一种颜色代表一条边,每一个拐角处都是让给了新的一条边来继续画——坚持了每条边左闭右开的原则——坚持 循环不变量原则。
【解答】:
/**
* @param {number} n
* @return {number[][]}
*/
var generateMatrix = function(n) {
let startX = startY = 0; // 起始位置
let loop = Math.floor(n / 2); // 旋转圈数
let mid = Math.floor(n / 2); // 中间位置
let offset = 1; // 控制每次填充元素个数
let count = 1; // 更新填充数字
let res = new Array(n).fill(0).map(() => new Array(n).fill(0)); // 定义一个二维数组
// 一个循环是一圈
while (loop--) {
let row = startX, col = startY;
// 上行从左到右(左闭右开)
while (col < startY + n - offset) {
res[row][col++] = count++;
}
// 右列从上到下(左闭右开)
while (row < startX + n - offset) {
res[row++][col] = count++;
}
// 下行从右到左(左闭右开)
while (col > startY) {
res[row][col--] = count++;
}
// 左列做下到上(左闭右开)
while (row > startX) {
res[row--][col] = count++;
}
// 更新起始位置
startX++;
startY++;
// 更新offset
offset += 2;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2 === 1) {
res[mid][mid] = count;
}
return res;
};
链表是一种通过指针串联在一起的线性结构。
链表的头结点是 head(是一个节点)。
链表重在注重边界的处理。
链表的操作:
查找链表的 2 种方式(✨✨✨✨✨):
head = head -> next
。定义一个链表:
// 先定义节点
class LinkNode {
val;
next;
// 节点的构造函数
constructor(val, next) {
this.val = val; // 节点上存储的元素
this.next = next; // 指向下一个节点的指针
}
}
// 再定义链表(链表包含:头指针、尾指针 和 链表长度)
class LinkList {
// 链表的构造函数
constructor() {
this._size = 0; // 索引值
this._head = null; // 头结点
this._tail = null; // 尾节点
}
// 在头节点前加入节点的方法
addAtHead(val) => {
let node = new LinkNode(val, this._head) // 初始化一个节点。
this._head = node // 把这个节点变成头结点
this._size++
if (!this._tail) { // 尾指针是否为空
this._tail = node // 当尾指针为 null 时,令这个节点:既是头指针,也是尾指针。
}
}
// 在末尾加入节点的方法
addAtTail(val) => {}
// 在链表中的第 index 个节点之前添加值为 val 的节点。
addAtIndex(val) => {}
// 根据索引获取节点,参数是 index 这里索引是从 0 开始的
getByIndex(val) => {}
// 在删掉索引处的节点
deleteAtIndex(val) => {}
}
// 初始化节点
var myLinkList = new LinkList()
// console.log(myLinkList)
// console.log(myLinkList._head.next)
插入/删除时间复杂度 | 查询时间复杂度 | 使用场景 | |
---|---|---|---|
数组 | O(n) | O(1) | 数据量固定(js 除外),频繁查询,较少增删 |
链表 | O(1) | O(n) | 数据量不固定,频繁增删,较少查询 |
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例:
// 输入
head = [1,2,6,3,4,5,6], val = 6
// 输出:[1,2,3,4,5]
// 输入
head = [], val = 1
// 输出:[]
// 输入
head = [7,7,7,7], val = 7
// 输出:[]
【解答】:
在原链表上直接删除链表元素:
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
if (head === null) return head;
while(head && head.val === val) {
head = head.next;
}
let cur = head;
while(cur && cur.next) {
if(cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
};
通过添加虚拟头节点来统一删除链表元素:
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
const virNode = new ListNode(0, head);
let cur = virNode;
while(cur.next) {
if(cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return virNode.next;
};
总结如下。
按照链表的 2 种不同的查找方式,找到对应的元素节点,将其删除。
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); // 链表变为1-> 2-> 3
linkedList.get(1); // 返回2
linkedList.deleteAtIndex(1); // 现在链表是1-> 3
linkedList.get(1); // 返回3
【解答】:
// 定义节点
class LinkNode {
constructor(val, next) {
this.val = val; // 值
this.next = next; // 后指针:指向的是下一个节点
}
}
// 定义联表
const MyLinkedList = function() {
this._size = 0; // 索引值
this._head = null; // 头结点
this._tail = null; // 尾节点
}
// 公共方法 - 获取当前节点
MyLinkedList.prototype.getCurrentNode = function(index){
if(index < 0 || index >= this._size) return null;
// 创建虚拟头节点
let cur = new LinkNode(0, this._head);
while(index >= 0) {
cur = cur.next;
index--;
}
return cur;
};
// 获取链表中第 index 个节点的值。如果索引无效,则返回 -1
MyLinkedList.prototype.get = function(index) {
if(index < 0 || index >= this._size) return -1;
// 获取当前节点
return this.getCurrentNode(index).val;
};
// 在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
MyLinkedList.prototype.addAtHead = function(val) {
const node = new LinkNode(val, this._head);
this._head = node;
this._size++;
if(!this._tail) {
this._tail = node;
}
};
// 将值为 val 的节点追加到链表的最后一个元素。
MyLinkedList.prototype.addAtTail = function(val) {
const node = new LinkNode(val, null); // 链表的最后一个节点指向的始终是 null。
if(this._tail) {
this._tail.next = node;
this._tail = node;
} else {
this._tail = node;
this._head = node;
}
this._size++;
};
// 在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
MyLinkedList.prototype.addAtIndex = function(index, val) {
if(index > this._size) return;
if(index <= 0) {
this.addAtHead(val);
return;
}
if(index === this._size) {
this.addAtTail(val);
return;
}
// 获取目标节点的上一个的节点
const node = this.getCurrentNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
};
// 如果索引 index 有效,则删除链表中的第 index 个节点。
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index < 0 || index >= this._size) return;
if(index === 0) {
this._head = this._head.next;
// 如果删除的这个节点同时是尾节点,要处理尾节点
if(index === this._size - 1){
this._tail = this._head
}
this._size--;
return;
}
// 获取目标节点的上一个的节点
const node = this.getCurrentNode(index - 1);
node.next = node.next.next;
// 处理尾节点
if(index === this._size - 1) {
this._tail = node;
}
this._size--;
};
// 初始化链表
const linkedList = new MyLinkedList()
// 使用链表
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); // 链表变为:1-> 2-> 3
linkedList.get(1); // 返回:2
linkedList.deleteAtIndex(1); // 现在链表是:1-> 3
linkedList.get(1); // 返回:3
【解析】:对“获取链表中第 index
个节点”的解析
总结如下。
注意:以下操作,凡是与 index 相关的,都是基于 index 是有效的情况来看的。
index
个节点,需要先创建虚拟头节点,然后循环遍历出 index 所对应的当前节点(详见上文中——对“获取链表中第 index 个节点”的解析)。index
个节点之前添加值为 val 的节点:
index
个节点,分两种情况:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
![在这里插入图片描述](https://img-blog.csdnimg.cn/4f9368c2708f469088f351c81e621715.png# _=270x)
// 输入
head = [1,2,3,4,5]
// 输出:[5,4,3,2,1]
解答:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
// 双指针法(推荐)
var reverseList = function(head) {
if(!head || !head.next) return head;
let cur = head;
let pre = null;
let temp = null; // 临时存储
while(cur) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
// 递归法
var reverse = function(pre, cur) {
if(!cur) return pre
let temp = cur.next
cur.next = pre
pre = cur
return reverse(pre, temp);
}
var reverseList = function(head) {
return reverse(null, head)
}
【解析】:用双指针法实现反转链表的解析
递归的写法是基于双指针的写法演变而来的。
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
// 输入
head = [1,2,3,4]
// 输出:[2,1,4,3]
解答:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var swapPairs = function(head) {
let virNode = new ListNode(0, head);
let cur = virNode;
while (cur.next && cur.next.next) {
let A = cur.next;
let B = cur.next.next;
A.next = B.next;
B.next = A;
cur.next = B;
cur = A;
}
return virNode.next;
};
【解析】:
先看链表的节点是奇数还是偶数个:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
// 输入
head = [1,2,3,4,5]
n = 2
// 输出:[1,2,3,5]
解答:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
// 双指针法(推荐)
var removeNthFromEnd = function(head, n) {
let virNode = new ListNode(0, head)
let F = S = virNode
while(n--) F = F.next
while(F.next) {
F = F.next
S = S.next
}
S.next = S.next.next
return virNode.next
};
// 递归倒退 n 法
var removeNthFromEnd = function(head, n) {
let virNode = new ListNode(0, head)
let index = 0
const recursion = (cur) => {
if (!cur) return
recursion(cur.next)
index++
if (index === n + 1) {
cur.next = cur.next.next
}
}
recursion(virNode)
return virNode.next
};
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
- 题目数据 保证 整个链式结构中不存在环。
- 注意,函数返回结果后,链表必须 保持其原始结构。
// 输入
intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
// 输出:Intersected at '8'
对上述示例代码的解释:
【解答】:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
// 双指针一法:同步走
// 都走 HeadA+headB 的长度,走完一个链表以后走另一个链表
var getIntersectionNode = function(headA, headB) {
// 有一个空则无交点
if(headA == null || headB == null) return null;
let pA = headA;
let pB = headB;
while(pA !== pB) {
pA = pA === null ? headB : pA.next
pB = pB === null ? headA : pB.next
}
return pA;
};
// 双指针二法:差步走
// 长度大的先走,走到和长度短的相同的位置后开始比较。
var getListLen = function(head) {
let len = 0, cur = head;
while(cur) {
len++;
cur = cur.next;
}
return len;
}
var getIntersectionNode = function(headA, headB) {
// 有一个空则无交点
if(headA == null || headB == null) return null;
let curA = headA;
let curB = headB;
// 求两表的长度
let lenA = getListLen(headA)
let lenB = getListLen(headB)
// 保证 A 是最长的那个链表,若 A 的长度小于 B 的长度则交换
if(lenA < lenB) {
// 下面交换变量注意加 “分号” ,两个数组交换变量在同一个作用域下时
// 如果不加分号,下面两条代码等同于一条代码: [curA, curB] = [lenB, lenA]
[curA, curB] = [curB, curA];
[lenA, lenB] = [lenB, lenA];
}
// 长度大的先走:让长的链表(A)先走与短的链表(B)相差的长度步数
let i = lenA - lenB;
while(i-- > 0) {
curA = curA.next;
}
// 长的链表(A)走到与短的链表(B)相同的位置后开始比较,不想等则同时往下走,直到相等为止。
while(curA && curA !== curB) {
curA = curA.next;
curB = curB.next;
}
return curA;
};
【解析】:
双指针一同步走法解析:
只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,此时返回 null。
当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
// 输入
head = [3,2,0,-4], pos = 1
// 输出:返回索引为 1 的链表节点
// 解释:链表中有一个环,其尾部连接到第二个节点。
【解答】:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
// 双指针法
var detectCycle = function(head) {
let F = head;
let S = head;
while(F !== null && F.next !== null) {
F = F.next.next; // 快指针,一次走两步
S = S.next; // 慢指针,一次走一步
// F 与 S 相等时则相遇,相遇则表示有环
if (F === S) {
let H = head;
let M = F; // 相遇的节点
// 相等则找到了入口节点,终止循环。否则 L 和 R 同时往下走一步
while(H !== M) {
H = H.next;
M = M.next;
}
return H;
}
}
return null;
};
【解析】:双指针法解析
解此题的关键在于——运用数学知识,找到恒等关系建立等式,并化简。
哈希表也有叫散列表的,指的是 “用关键码值直接访问数据” 的数据结构。
类比数组理解哈希表数据结构:哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
三种哈希结构:
哈希表的特点:
Set | Map | |||
---|---|---|---|---|
属性 | 描述 | 属性 | 描述 | |
构造函数 | Set() | 创建一个新的 Set 对象。 | Map() | 创建 Map 对象。 |
实例属性 | Set.prototype.size | 返回 Set 对象中的值的个数。 | Map.prototype.size | 返回 Map 对象中的键值对数量。 |
实例方法 | Set.prototype.add(value) | 在Set对象尾部添加一个元素。返回该 Set 对象。 | - | - |
- | - | Map.prototype.get(key) | 返回与 key 关联的值,若不存在关联的值,则返回 undefined。 | |
- | - | Map.prototype.set(key, value) | 在 Map 对象中设置与指定的键 key 关联的值 value,并返回 Map 对象。 | |
Set.prototype.clear() | 移除Set对象内的所有元素。 | Map.prototype.clear() | 移除 Map 对象中所有的键值对。 | |
Set.prototype.delete(value) | 移除值为 value 的元素,并返回一个布尔值来表示是否移除成功。Set.prototype.has(value) 会在此之后返回 false。 | Map.prototype.delete(key) | 移除 Map 对象中指定的键值对,如果键值对存在并成功被移除,返回 true,否则返回 false。调用 delete 后再调用 Map.prototype.has(key) 将返回 false。 | |
Set.prototype.has(value) | 返回一个布尔值,表示该值在 Set 中存在与否。 | Map.prototype.has(key) | 返回一个布尔值,用来表明 Map 对象中是否存在与 key 关联的值。 | |
Set.prototype[@@iterator]() | 返回一个新的迭代器对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值。 | Map.prototype[@@iterator]() | 返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,并以插入 Map 对象的顺序排列。 | |
Set.prototype.keys() (en-US) | 与 values() 方法相同,返回一个新的迭代器对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值。 | Map.prototype.keys() | 返回一个新的迭代对象,其中包含 Map 对象中所有的键,并以插入 Map 对象的顺序排列。 | |
Set.prototype.values() | 返回一个新的迭代器对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值。 | Map.prototype.values() | 返回一个新的迭代对象,其中包含 Map 对象中所有的值,并以插入 Map 对象的顺序排列。 | |
Set.prototype.entries() | 返回一个新的迭代器对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值的 [value, value] 数组。为了使这个方法和 Map 对象保持相似, 每个值的键和值相等。 | Map.prototype.entries() | 返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,并以插入 Map 对象的顺序排列。 | |
Set.prototype.forEach(callbackFn[, thisArg]) | 按照插入顺序,为 Set 对象中的每一个值调用一次 callBackFn。如果提供了thisArg参数,回调中的 this 会是这个参数。 | Map.prototype.forEach(callbackFn[, thisArg]) | 以插入的顺序对 Map 对象中存在的键值对分别调用一次 callbackFn。如果给定了 thisArg 参数,这个参数将会是回调函数中 this 的值。 |
Set 使用案例
Map使用案例
什么时候使用哈希法?
例如:要查询一个名字是否在这所学校里——我们需要把这所学校里学生的名字存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表的实现过程 就是——哈希函数。
1⃣️、把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
2⃣️、如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置——这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法:
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例:
// 输入
s = "anagram", t = "nagaram"
// 输出:true
// 输入
s = "rat", t = "car"
// 输出:false
解答:
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isAnagram = function(s, t) {
if(s.length !== t.length) return false;
let arr = new Array(26).fill(0);
let base = 'a'.charCodeAt(0);
for(let i = 0; i < s.length; i++) {
arr[s[i].charCodeAt() - base]++;
arr[t.charCodeAt(i) - base]--;
}
return arr.every(item => item === 0);
};
上述代码中,s[i].charCodeAt()
和 t.charCodeAt(i)
表示的意思是一样的,只是写法不同——数组 s 或 t 中的第 i 项对应的 ASCII 码值。
【解析】:
数组是一个简单哈希表,这道题目中字符串只有小写字符,那么就可以定义一个大小为 26 且初始化为 0 的数组(因为字符 a 到字符 z 的 ASCII 也是 26 个连续的数值),来记录某个字符在字符串 s 或 t 里出现的次数。
然后,需要把字符映射到数组也就是哈希表的索引下标上,因为字符 a 到字符 z 的 ASCII 是 26 个连续的数值,所以字符 a 映射为下标 0,相应的字符 z 映射为下标 25。
在遍历时候需要:将 s 中出现的字符映射到哈希表索引上的数值做 +1 操作,将 t 中出现的字符映射到哈希表索引上的数值做 -1 的操作。
最后检查一下:如果数组中有的元素不为零 0,说明字符串 s 和 t 一定是谁多了字符或者谁少了字符,返回 false。如果数组所有元素都为 0,说明字符串 s 和 t 是字母异位词,返回 true。
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例:
// 输入
nums1 = [4,9,5], nums2 = [9,4,9,8,4]
// 输出:[9,4]
// 解释:[4,9] 也是可通过的
【解答】:
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
if(!nums1.length || !nums2.length) return []
if(nums1.length < nums2.length) {
let list = nums1
nums1 = nums2
nums2 = list
}
let setList = new Set(nums1) // 对较长的数组去重
let resSet = new Set()
for (let i = 0; i <= nums2.length; i++) {
setList.has(nums2[i]) && resSet.add(nums2[i])
}
return [...resSet];
};
总结如下。
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例:
// 输入
n = 19
// 输出:true
/** 解释:
* 12 + 92 = 82
* 82 + 22 = 68
* 62 + 82 = 100
* 12 + 02 + 02 = 1
**/
// 输入
n = 2
// 输出:false
【解答】:
/**
* @param {number} n
* @return {boolean}
*/
// Set
var isHappy = function(n) {
function getSum (n) {
let sum = 0
while(n > 0) {
sum += (n % 10) ** 2
n = Math.floor(n / 10)
}
return sum
}
let resSet = new Set()
while(n > 0) {
if (n === 1) return true
if (resSet.has(n)) return false // n 出现过,证明已陷入无限循环
resSet.add(n)
n = getSum(n)
}
};
// Map
var isHappy = function(n) {
function getSum (n) {
let sum = 0
while(n > 0) {
sum += (n % 10) ** 2
n = Math.floor(n / 10)
}
return sum
}
let resMap = new Map()
while(n > 0) {
if (n === 1) return true
if (resMap.has(n)) return false // n 出现过,证明已陷入无限循环
resMap.set(n, 1) // 这一点与 Set 不同
n = getSum(n)
}
};
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例:
// 输入
nums = [2,7,11,15], target = 9
// 输出:[0,1]
// 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
【解题】:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
// Map hash 解法
var twoSum = function(nums, target) {
let mHash = new Map()
let i = 0
while(i < nums.length) {
let index = mHash.get(target - nums[i])
if (index !== undefined) {
return [i, index]
}
mHash.set(nums[i], i)
i++
}
};
// 双指针解法
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
let i = 0 // 慢指针
let j = 1 // 快指针
while(i < nums.length) {
if (nums[i] + nums[j] === target) {
return [i, j];
} else if (j < nums.length - 1) {
j++;
} else {
i++;
j = i + 1;
}
};
};
对比下 hash 和 双指针 的耗时:
不难发现,hash 比双指针快了 2 倍。
【解析】:Map 哈希实现两数之和的步骤解析:
用 Map 做哈希一定要弄清楚 2 点:
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素比配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
为什么会选择 Map 做哈希呢?请看下面的总结。
当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
使用 数组 和 set 来做哈希法的局限性:
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。
队列(queue)是先进先出——管子。
栈(stack)是先进后出——杯子。
括号匹配是使用栈解决的经典问题。匹配问题都是栈的强项。
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 注意空字符串可被认为是有效字符串。
示例:
// 输入: "()"
// 输出: true
// 输入: "()[]{}"
// 输出: true
// 输入: "(]"
// 输出: false
// 输入: "([)]"
// 输出: false
// 输入: "{[]}"
// 输出: true
解答:
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const stack = []
const map = {
'(': ')',
'[': ']',
'{': '}'
}
for (const item of s) {
if (item in map) {
stack.push(item)
continue
}
if(map[stack.pop()] !== item) return false;
}
return !stack.length;
}
工作上一定没人这么搞,但是考察对栈、队列理解程度的好题。
使用栈实现队列的下列操作:
- push(x):将一个元素放入队列的尾部。
- pop():从队列首部移除元素。
- peek():返回队列首部的元素。
- empty():返回队列是否为空。
说明:
- 你只能使用标准的栈操作 – 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
- 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。
示例:
// 输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
// 输出:
[null, null, null, 1, 1, false]
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
解答:
// JS 版
/**
* 在此处初始化数据结构。
*/
var MyQueue = function() {
// 使用两个数组的栈方法(push, pop) 实现队列
this.stackIn = [];
this.stackOut = [];
};
/**
* 将元素 x 推到队列的后面。
* @param {number} x
* @return {void}
*/
MyQueue.prototype.push = function(x) {
this.stackIn.push(x);
};
/**
* 从队列前面移除元素并返回该元素。
* @return {number}
*/
MyQueue.prototype.pop = function() {
const size = this.stackOut.length;
if(size) {
return this.stackOut.pop();
}
while(this.stackIn.length) {
this.stackOut.push(this.stackIn.pop());
}
return this.stackOut.pop();
};
/**
* 获取前端元素。
* @return {number}
*/
MyQueue.prototype.peek = function() {
const x = this.pop();
this.stackOut.push(x);
return x;
};
/**
* 返回队列是否为空。
* @return {boolean}
*/
MyQueue.prototype.empty = function() {
return !this.stackIn.length && !this.stackOut.length
};
// TS 版
class MyQueue {
// 使用两个数组的栈方法(push, pop) 实现队列
private stackIn: number[]
private stackOut: number[]
constructor() {
this.stackIn = [];
this.stackOut = [];
}
push(x: number): void {
this.stackIn.push(x);
}
pop(): number {
if (this.stackOut.length === 0) {
while (this.stackIn.length > 0) {
this.stackOut.push(this.stackIn.pop()!);
}
}
return this.stackOut.pop()!;
}
peek(): number {
let temp: number = this.pop();
this.stackOut.push(temp);
return temp;
}
empty(): boolean {
return this.stackIn.length === 0 && this.stackOut.length === 0;
}
}
使用队列实现栈的下列操作:
- push(x):元素 x 入栈。
- pop():移除栈顶元素。
- top():获取栈顶元素。
- empty():返回栈是否为空。
注意:
- 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
- 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
- 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
示例:
// 输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
// 输出:
[null, null, null, 2, 2, false]
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
解答:
【使用两个队列实现】
// JS 版
/**
* 在此处初始化数据结构。
*/
var MyStack = function() {
this.queue1 = [];
this.queue2 = [];
};
/**
* 将元件 x 推到堆叠上
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
this.queue1.push(x);
};
/**
* 移除堆栈顶部的元素并返回该元素。
* @return {number}
*/
MyStack.prototype.pop = function() {
// 减少两个队列交换的次数, 只有当queue1为空时,交换两个队列
if(!this.queue1.length) {
[this.queue1, this.queue2] = [this.queue2, this.queue1];
}
while(this.queue1.length > 1) {
this.queue2.push(this.queue1.shift());
}
return this.queue1.shift();
};
/**
* 获取顶部元素。
* @return {number}
*/
MyStack.prototype.top = function() {
const x = this.pop();
this.queue1.push(x);
return x;
};
/**
* 返回堆栈是否为空。
* @return {boolean}
*/
MyStack.prototype.empty = function() {
return !this.queue1.length && !this.queue2.length;
};
// TS 版
class MyStack {
private queue: number[];
private tempQueue: number[];
constructor() {
this.queue = [];
this.tempQueue = [];
}
push(x: number): void {
this.queue.push(x);
}
pop(): number {
for (let i = 0, length = this.queue.length - 1; i < length; i++) {
this.tempQueue.push(this.queue.shift()!);
}
let res: number = this.queue.pop()!;
let temp: number[] = this.queue;
this.queue = this.tempQueue;
this.tempQueue = temp;
return res;
}
top(): number {
let res: number = this.pop();
this.push(res);
return res;
}
empty(): boolean {
return this.queue.length === 0;
}
}
【使用一个队列实现】
// JS 版
/**
* 在此处初始化数据结构。
*/
var MyStack = function() {
this.queue = [];
};
/**
* 将元件x推到堆叠上。
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
this.queue.push(x);
};
/**
* 移除堆栈顶部的元素并返回该元素。
* @return {number}
*/
MyStack.prototype.pop = function() {
let size = this.queue.length;
while(size-- > 1) {
this.queue.push(this.queue.shift());
}
return this.queue.shift();
};
/**
* 获取顶部元素。
* @return {number}
*/
MyStack.prototype.top = function() {
const x = this.pop();
this.queue.push(x);
return x;
};
/**
* 返回堆栈是否为空。
* @return {boolean}
*/
MyStack.prototype.empty = function() {
return !this.queue.length;
};
// TS 版
class MyStack {
private queue: number[];
constructor() {
this.queue = [];
}
push(x: number): void {
this.queue.push(x);
}
pop(): number {
for (let i = 0, length = this.queue.length - 1; i < length; i++) {
this.queue.push(this.queue.shift()!);
}
return this.queue.shift()!;
}
top(): number {
let res: number = this.pop();
this.push(res);
return res;
}
empty(): boolean {
return this.queue.length === 0;
}
}
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 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 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
**/
// 输入:
nums = [1], k = 1
// 输出:[1]
解答:
// JS 版
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
class MonoQueue {
queue;
constructor() {
this.queue = [];
}
enqueue(value) {
let back = this.queue[this.queue.length - 1];
while (back !== undefined && back < value) {
this.queue.pop();
back = this.queue[this.queue.length - 1];
}
this.queue.push(value);
}
dequeue(value) {
let front = this.front();
if (front === value) {
this.queue.shift();
}
}
front() {
return this.queue[0];
}
}
let helperQueue = new MonoQueue();
let i = 0, j = 0;
let resArr = [];
while (j < k) {
helperQueue.enqueue(nums[j++]);
}
resArr.push(helperQueue.front());
while (j < nums.length) {
helperQueue.enqueue(nums[j]);
helperQueue.dequeue(nums[i]);
resArr.push(helperQueue.front());
i++, j++;
}
return resArr;
};
// TS 版
function maxSlidingWindow(nums: number[], k: number): number[] {
/** 单调递减队列 */
class MonoQueue {
private queue: number[];
constructor() {
this.queue = [];
};
/** 入队:value如果大于队尾元素,则将队尾元素删除,直至队尾元素大于value,或者队列为空 */
public enqueue(value: number): void {
let back: number | undefined = this.queue[this.queue.length - 1];
while (back !== undefined && back < value) {
this.queue.pop();
back = this.queue[this.queue.length - 1];
}
this.queue.push(value);
};
/** 出队:只有当队头元素等于value,才出队 */
public dequeue(value: number): void {
let top: number | undefined = this.top();
if (top !== undefined && top === value) {
this.queue.shift();
}
}
public top(): number | undefined {
return this.queue[0];
}
}
const helperQueue: MonoQueue = new MonoQueue();
let i: number = 0,
j: number = 0;
let resArr: number[] = [];
while (j < k) {
helperQueue.enqueue(nums[j++]);
}
resArr.push(helperQueue.top()!);
while (j < nums.length) {
helperQueue.enqueue(nums[j]);
helperQueue.dequeue(nums[i]);
resArr.push(helperQueue.top()!);
j++, i++;
}
return resArr;
};
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
示例:
// 输入:
temperatures = [73,74,75,71,69,72,76,73]
// 输出: [1,1,4,2,1,1,0,0]
// 输入:
temperatures = [30,40,50,60]
// 输出: [1,1,1,0]
// 输入:
temperatures = [30,60,90]
// 输出: [1,1,0]
解答:
var dailyTemperatures = function(temperatures) {
const n = temperatures.length;
const res = Array(n).fill(0);
const stack = []; // 递增栈:用于存储元素右面第一个比他大的元素下标
stack.push(0);
for (let i = 1; i < n; i++) {
while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const top = stack.pop();
res[top] = i - top;
}
stack.push(i);
}
return res;
};
递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
// 输入:"abbaca"
// 输出:"ca"
// 解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。
// 之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
解答:
【使用栈】
var removeDuplicates = function(s) {
const stack = [];
for(const x of s) {
let c = null;
if(stack.length && x === (c = stack.pop())) continue;
c && stack.push(c);
stack.push(x);
}
return stack.join("");
};
【双指针(模拟栈)】
var removeDuplicates = function(s) {
s = [...s];
let top = -1; // 指向栈顶元素的下标
for(let i = 0; i < s.length; i++) {
if(top === -1 || s[top] !== s[i]) { // top === -1 即空栈
s[++top] = s[i]; // 入栈
} else {
top--; // 推出栈
}
}
s.length = top + 1; // 栈顶元素下标 + 1 为栈的长度
return s.join('');
};
优先级队列其实就是一个披着队列外衣的堆。因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
提示:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O ( n log n ) O(n \log n) O(nlogn) , n 是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案。
示例:
// 输入:
nums = [1,1,1,2,2,3], k = 2
// 输出: [1,2]
// 输入:
nums = [1], k = 1
// 输出: [1]
解答:
// JS 版
// js 没有堆 需要自己构造
class Heap {
constructor(compareFn) {
this.compareFn = compareFn;
this.queue = [];
}
// 添加
push(item) {
// 推入元素
this.queue.push(item);
// 上浮
let index = this.size() - 1; // 记录推入元素下标
let parent = Math.floor((index - 1) / 2); // 记录父节点下标
while (parent >= 0 && this.compare(parent, index) > 0) { // 注意compare参数顺序
[this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]];
// 更新下标
index = parent;
parent = Math.floor((index - 1) / 2);
}
}
// 获取堆顶元素并移除
pop() {
// 堆顶元素
const out = this.queue[0];
// 移除堆顶元素 填入最后一个元素
this.queue[0] = this.queue.pop();
// 下沉
let index = 0; // 记录下沉元素下标
let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标
let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
while (searchChild !== undefined && this.compare(index, searchChild) > 0) {
// 注意compare参数顺序
[this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]];
// 更新下标
index = searchChild;
left = 2 * index + 1;
searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
}
return out;
}
size() {
return this.queue.length;
}
// 使用传入的 compareFn 比较两个位置的元素
compare(index1, index2) {
// 处理下标越界问题
if (this.queue[index1] === undefined) return 1;
if (this.queue[index2] === undefined) return -1;
return this.compareFn(this.queue[index1], this.queue[index2]);
}
}
const topKFrequent = function (nums, k) {
const map = new Map();
for (const num of nums) {
map.set(num, (map.get(num) || 0) + 1);
}
// 创建小顶堆
const heap= new Heap((a, b) => a[1] - b[1]);
// entry 是一个长度为2的数组,0位置存储key,1位置存储value
for (const entry of map.entries()) {
heap.push(entry);
if (heap.size() > k) {
heap.pop();
}
}
// return heap.queue.map(e => e[0]);
const res = [];
for (let i = heap.size() - 1; i >= 0; i--) {
res[i] = heap.pop()[0];
}
return res;
};
// TS 版
function topKFrequent(nums: number[], k: number): number[] {
const countMap: Map<number, number> = new Map();
for (let num of nums) {
countMap.set(num, (countMap.get(num) || 0) + 1);
}
// tS没有最小堆的数据结构,所以直接对整个数组进行排序,取前k个元素
return [...countMap.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, k)
.map(i => i[0]);
};
深入学习二叉树请戳这里
二叉树的定义和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。
例如:
// JS 版
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
// TS 版
class TreeNode {
public val: number;
public left: TreeNode | null;
public right: TreeNode | null;
constructor(val?: number, left?: TreeNode, right?: TreeNode) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
二叉树有两种主要的形式:满二叉树 和 完全二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树:是一个有序树——是有数值的树。
平衡二叉搜索树又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:
二叉树可以链式存储,也可以顺序存储。
二叉树的顺序存储其实就是用数组来存储二叉树。
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
二叉树主要有两种遍历方式:
递归三要素:
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例:
// 输入:
root = [1,null,2,3]
// 输出:[1,2,3]
// 输入:
root = []
// 输出:[]
// 输入:
root = [1]
// 输出:[1]
解答:
【递归】
var preorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null)return ;
//先序遍历所以从父节点开始
res.push(root.val);
//递归左子树
dfs(root.left);
//递归右子树
dfs(root.right);
}
//只使用一个参数 使用闭包进行存储结果
dfs(root);
return res;
};
【迭代】
// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function(root, res = []) {
if(!root) return res;
const stack = [root];
let cur = null;
while(stack.length) {
cur = stack.pop();
res.push(cur.val);
cur.right && stack.push(cur.right);
cur.left && stack.push(cur.left);
}
return res;
};
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例:
// 输入:
root = [1,null,2,3]
// 输出:[1,3,2]
// 输入:
root = []
// 输出:[]
// 输入:
root = [1]
// 输出:[1]
解答:
【递归】
var inorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null){
return ;
}
dfs(root.left);
res.push(root.val);
dfs(root.right);
}
dfs(root);
return res;
};
【迭代】
// 入栈 左 -> 右
// 出栈 左 -> 中 -> 右
var inorderTraversal = function(root, res = []) {
const stack = [];
let cur = root;
while(stack.length || cur) {
if(cur) {
stack.push(cur);
// 左
cur = cur.left;
} else {
// --> 弹出 中
cur = stack.pop();
res.push(cur.val);
// 右
cur = cur.right;
}
};
return res;
};
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
示例:
// 输入:
root = [1,null,2,3]
// 输出:[3,2,1]
// 输入:
root = []
// 输出:[]
// 输入:
root = [1]
// 输出:[1]
解答:
【递归】
var postorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null){
return ;
}
dfs(root.left);
dfs(root.right);
res.push(root.val);
}
dfs(root);
return res;
};
【迭代】
// 入栈 左 -> 右
// 出栈 中 -> 右 -> 左 结果翻转
var postorderTraversal = function(root, res = []) {
if (!root) return res;
const stack = [root];
let cur = null;
do {
cur = stack.pop();
res.push(cur.val);
cur.left && stack.push(cur.left);
cur.right && stack.push(cur.right);
} while(stack.length);
return res.reverse();
};
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯法就是暴力搜索,并不是什么高效的算法。
回溯法,一般可以解决如下几种问题:
回溯法确实不好理解,所以需要把回溯法抽象为一个树形结构图来理解就容易多了。具体结构剖析请参阅这里。
贪心算法并没有固定的套路,唯一的难点就是:如何通过局部最优,推出整体最优。
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心算法一般分为如下四步:
具体请参阅这里。
如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。不用死扣动规和贪心的理论区别,做做题目自然就知道了。
动态规划的五步曲:
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
做动态规划题目的灵魂三问:
找 动态规划 的问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的。
具体请参阅这里。
【参考文章】
leecode
用 js 写一个链表(详细注释)
代码随想录