学习算法和刷题的框架思维
2021-07-20
0. 参考文章
1. 只有两种数据结构的存储方式
- 数组(顺序存储)
- 链表(链式存储)
而数据结构则可以用不同的存储方式去实现。
比如「图」的两种表示方法,邻接表就是链表,邻接矩阵就是二维数组。
数组可以节省空间,但是操作往往比较复杂(因为需要缩容扩容),而链表操作简单,但所需空间更多(要存储指针)。
Redis 提供列表、字符串、集合等等几种常用数据结构,但是对于每种数据结构,底层的存储方式都至少有两种,以便于根据存储数据的实际情况使用合适的存储方式。
我们常常会因为看到各种数据结构的名词而觉得头晕,其实换个角度思考,不管什么数据结构,都要用这两种存储方式来实现,这样一来是不是就豁然开朗了呢~
数组和链表的优缺点
数组(连续存储)
- 优点:连续存储,通过 index 来定位元素,节约存储空间
- 缺点:
- 内存空间必须一次性分配够,假如需要扩容,则重新分配+复制原有数据,时间复杂度 O(N)
- 数组中间元素的插入和删除会涉及到搬运其他数据以保持连续,时间复杂度 O(N)
链表(存储空间不连续)
- 优点:不存在数组扩容问题,只要知道某元素的 previous node 和 next node,就可以通过操作指针来删除/插入新元素,时间复杂度 O(1)
- 缺点:无法根据 index 来算出元素的地址,无法随机访问;需要更多的存储空间来存每个元素前后元素位置的指针信息。
2. 四种数据结构的操作方式
之前也做过类似的总结,无外乎遍历+访问,也就是增删查改这四个基本操作。
数据结构存在的意义,就是在不同的场景下满足高效的增删查改需求。
遍历 + 访问的两种形式
- 线性(迭代 for/while)
- 非线性(递归 recursive)
几种遍历框架
数组(线性迭代)
void traverse(int[] arr) {
for (int i = 0; i < arr.length; i++) {
// 迭代访问 arr[i]
}
}
链表(线性迭代)
/* 基本的单链表节点 */
class ListNode {
int val;
ListNode next;
}
void traverse(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
// 迭代访问 p.val
}
}
链表(非线性递归)
/* 基本的单链表节点 */
class ListNode {
int val;
ListNode next;
}
void traverse(ListNode head) {
// 递归访问 head.val
traverse(head.next);
}
二叉树(非线性递归)
/* 基本的二叉树节点 */
class TreeNode {
int val;
TreeNode left, right;
}
void traverse(TreeNode root) {
// 前序遍历代码位置
traverse(root.left)
// 中序遍历代码位置
traverse(root.right)
// 后序遍历代码位置
}
N 叉树(非线性递归)
/* 基本的 N 叉树节点 */
class TreeNode {
int val;
TreeNode[] children;
}
void traverse(TreeNode root) {
for (TreeNode child : root.children)
traverse(child);
}
图(非线性递归)
Todo: 补充图的遍历框架
N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 visited 做标记就行了.
3. 怎么刷 leetcode?
数据结构是工具,算法是通过合适的工具解决特定问题的方法。
学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。
作者建议先刷二叉树,因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题。
刷完整个「二叉树专题」再去做「回溯、动规、分治专题」,因为只要涉及递归的问题,都是树的问题。