一、数据结构的存储方式
数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。
散列表、堆、栈、队列、树、图等其他数据结构,可以理解为【上层建筑】,而数组和链表才是【结构基础】。那些多样化的数据结构,究其源头,都是在链表或则数组上做特殊操作,API不同而已。
比如说,【队列】、【栈】这两种数据结构既可以使用链表也可以使用数据实现。用数组实现就i要处理扩容问题;用链表实现,就不需要,但是需要更多的内存空间存储节点指针。
【图】的两种表示方法,邻接表就是链表,邻接矩阵就是二维数据。邻接矩阵就是二维数组,判断连通性迅速,并可以使用矩阵矩阵运算解决问题;邻接表节省空间,但是很多操作效率更低。
【散列表】就是通过散列函数把键映射到一个大数组里。而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单,但需要额外的空间存储指针;线性探测法就需要数组特性,以便连续寻址,不需要指针的存储空间,但操作稍微复杂。
【树】,用数组实现就是【堆】,【堆】就是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单;常见的【树】以链表实现。
综上,两者的优缺点:
数组:连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约空间。但是扩容、删除和插入操作时间复杂度为O(n)
链表:元素不连续,不存在扩容问题;插入和删除操作O(1);不能随机访问,相对而言更耗内存。
二、数据结构的基本操作
无非是遍历+访问,即增删改减。
数据结构种类很多,但它们存在的目的都是在不同的应用场景,尽可能高效地增删查改。
我们仍然从最高层来看,各种数据结构的遍历 + 访问无非两种形式:线性的和非线性的。
数组遍历框架,典型的线性迭代框架:
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
}
}
void traverse(ListNode head) {
// 递归访问 head.val
traverse(head.next);
}
所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构
三、刷题步骤
1、先学习像数组、链表这种基本数据结构的常用算法
像单链表翻转,前缀和数组,二分搜索等,因为这些算法属于会者不难难者不会的类型,难度不大,学习它们不会花费太多时间。而且这些小而美的算法经常让你大呼精妙,能够有效培养你对算法的兴趣。
2、先刷二叉树,先刷二叉树,先刷二叉树,重要的事情说三遍
学会基础算法之后,不要急着上来就刷回溯算法、动态规划这类笔试常考题
刷二叉树看到题目没思路?根据很多读者的问题,其实大家不是没思路,只是没有理解我们说的「框架」是什么
void traverse(TreeNode root) {
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
比如说我随便拿几道题的解法出来,不用管具体的代码逻辑,只要看看框架在其中是如何发挥作用的就行。
力扣第 124 题,难度困难,让你求二叉树中最大路径和,主要代码如下:
int res = Integer.MIN_VALUE;
int oneSideMax(TreeNode root) {
if (root == null) return 0;
int left = max(0, oneSideMax(root.left));
int right = max(0, oneSideMax(root.right));
// 后序位置
res = Math.max(res, left + right + root.val);
return Math.max(left, right) + root.val;
}
注意递归函数的位置,这就是个后序遍历嘛,无非就是把 traverse
函数名字改成 oneSideMax
了
力扣第 105 题,难度中等,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下:
TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd) {
// 前序位置,寻找左右子树的索引
if (preStart > preEnd) {
return null;
}
int rootVal = preorder[preStart];
int index = 0;
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootVal) {
index = i;
break;
}
}
int leftSize = index - inStart;
TreeNode root = new TreeNode(rootVal);
// 递归构造左右子树
root.left = build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, index - 1);
root.right = build(preorder, preStart + leftSize + 1, preEnd,
inorder, index + 1, inEnd);
return root;
}
不要看这个函数的参数很多,只是为了控制数组索引而已。注意找递归函数 build
的位置,本质上该算法也就是一个前序遍历,因为它在前序遍历的位置加了一坨代码逻辑。
力扣第 230 题,难度中等,寻找二叉搜索树中的第 k
小的元素,主要代码如下:
int res = 0;
int rank = 0;
void traverse(TreeNode root, int k) {
if (root == null) {
return;
}
traverse(root.left, k);
/* 中序遍历代码位置 */
rank++;
if (k == rank) {
res = root.val;
return;
}
/*****************/
traverse(root.right, k);
}
这不就是个中序遍历嘛,对于一棵 BST 中序遍历意味着什么,应该不需要解释了吧。
对于一个理解二叉树的人来说,刷一道二叉树的题目花不了多长时间。那么如果你对刷题无从下手或者有畏惧心理,不妨从二叉树下手,前 10 道也许有点难受;结合框架再做 20 道,也许你就有点自己的理解了;刷完整个专题,再去做什么回溯动规分治专题,你就会发现只要涉及递归的问题,都是树的问题。
从框架上看问题,就是像我们这样基于框架进行抽取和扩展,既可以在看别人解法时快速理解核心逻辑,也有助于找到我们自己写解法时的思路方向。
这种思维是很重要的, 动态规划详解 中总结的找状态转移方程的几步流程,有时候按照流程写出解法,可能自己都不知道为啥是对的,反正它就是对了。。
这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别。
本文总结:
数据结构的基本存储方式就是链式和顺序两种,基本操作就是增删查改,遍历方式无非迭代和递归。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报