随笔- 20  文章- 0  评论- 3  阅读- 9803 

一、数据结构的存储方式

  数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。

  散列表、堆、栈、队列、树、图等其他数据结构,可以理解为【上层建筑】,而数组和链表才是【结构基础】。那些多样化的数据结构,究其源头,都是在链表或则数组上做特殊操作,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 道,也许你就有点自己的理解了;刷完整个专题,再去做什么回溯动规分治专题,你就会发现只要涉及递归的问题,都是树的问题

  从框架上看问题,就是像我们这样基于框架进行抽取和扩展,既可以在看别人解法时快速理解核心逻辑,也有助于找到我们自己写解法时的思路方向。

  这种思维是很重要的, 动态规划详解 中总结的找状态转移方程的几步流程,有时候按照流程写出解法,可能自己都不知道为啥是对的,反正它就是对了。。

  这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别

  本文总结:

  数据结构的基本存储方式就是链式和顺序两种,基本操作就是增删查改,遍历方式无非迭代和递归。

 posted on   Slothhh  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
点击右上角即可分享
微信分享提示