一、常用数据结构
数组、字符串(Array & String)
数组的优缺点
要掌握一种数据结构,就必须要懂得分析它的优点和缺点。
数组的优点在于:
构建非常简单
能在 O(1) 的时间里根据数组的下标(index)查询某个元素
数组的缺点在于:
构建时必须分配一段连续的空间
查询某个元素是否存在时需要遍历整个数组,耗费 O(n) 的时间(其中,n 是元素的个数)
删除和添加某个元素时,同样需要耗费 O(n) 的时间
链表(LinkedList)
单链表:链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
双链表:与单链表不同的是,双链表的每个结点中都含有两个引用字段。
链表的优缺点
链表的优点如下:
- 链表能灵活地分配内存空间;
- 能在 O(1) 时间内删除或者添加元素,前提是该元素的前一个元素已知,当然也取决于是单链表还是双链表,在双链表中,如果已知该元素的后一个元素,同样可以在 O(1) 时间内删除或者添加该元素。
链表的缺点是:
- 不像数组能通过下标迅速读取元素,每次都要从链表头开始一个一个读取;
- 查询第 k 个元素需要 O(k) 时间。
应用场景:
如果要解决的问题里面需要很多快速查询,链表可能并不适合;如果遇到的问题中,数据的元素个数不确定,而且需要经常进行数据的添加和删除,那么链表会比较合适。而如果数据元素大小确定,删除插入的操作并不多,那么数组可能更适合。
经典解法
链表是实现很多复杂数据结构的基础,经典解法如下。
1. 利用快慢指针(有时候需要用到三个指针)
典型题目例如:链表的翻转,寻找倒数第 k 个元素,寻找链表中间位置的元素,判断链表是否有环等等。
2. 构建一个虚假的链表头
一般用在要返回新的链表的题目中,比如,给定两个排好序的链表,要求将它们整合在一起并排好序。又比如,将一个链表中的奇数和偶数按照原定的顺序分开后重新组合成一个新的链表,链表的头一半是奇数,后一半是偶数。
在这类问题里,如果不用一个虚假的链表头,那么在创建新链表的第一个元素时,我们都得要判断一下链表的头指针是否为空,也就是要多写一条 if else 语句。比较简洁的写法是创建一个空的链表头,直接往其后面添加元素即可,最后返回这个空的链表头的下一个节点即可。
例题:K 个一组翻转链表
const myReverse = (head, tail) => {
let prev = tail.next;//prev永远指向翻转后的头部
let p = head;//p永远指向未翻转的头部
while (prev !== tail) {//只要prev还没循环到原链表的尾部,就继续循环
const nex = p.next;//保存好下一个循环的未翻转的头部
p.next = prev;//将未翻转的头部移到翻转后的头部
prev = p;
p = nex;//p指针回到未翻转的头部
}
return [tail, head];//将翻转好的子链表返回
}
var reverseKGroup = function(head, k) {
const hair = new ListNode(0);//创建一个虚假的链表头
hair.next = head;
let pre = hair;
while (head) {
let tail = pre;//让tail指针停在0刻度
// 查看剩余部分长度是否大于等于 k
for (let i = 0; i < k; ++i) {
tail = tail.next;
if (!tail) {
return hair.next;
}
}
const nex = tail.next;
[head, tail] = myReverse(head, tail);
// 把子链表重新接回原链表
pre.next = head;
tail.next = nex;
pre = tail;
head = tail.next;
}
return hair.next;
};
栈(Stack)
特点:后进先出(LIFO)
实现:利用单链表
例题:有效的括号
/**
* @param {string}
* @return {boolean}
*/
var isValid = function (s) {
let len = s.length;
let first = s[0];
if (len === 0) return true;
// 奇数或右括号开头肯定不符合
if (len % 2 != 0 || [')', ']', '}'].indexOf(first) != -1) {
return false
}
// 栈,存入第一个字符。
let stack = [first];
const MAP = {
')': '(',
']': '[',
'}': '{',
}
for (let i = 1; i < len; i++) {
let length = stack.length;
let top = length > 0 ? stack[length - 1] : null;
let now = s[i];
if (MAP[now] === top) {
stack.pop();
} else {
stack.push(now);
}
}
return stack.length === 0
};
例题:每日温度
单调栈
什么时候用:
- 通常是一维数组,要寻找任一元素右边(左边)第一个比自己大(小)的元素
- 且要求 O(n) 的时间复杂度
在栈里存入数组的下标
利用堆栈,还可以解决如下常见问题:
- 求解算术表达式的结果(LeetCode 224、227、772、770)
- 求解直方图里最大的矩形区域(LeetCode 84)