【labuladong的算法小抄】0. 学习数据结构和算法的思维框架

一、数据结构的存储方式

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

不是还有散列表、栈、队列、堆、树、图等数据结构吗?

这些都是术(上层建筑),而数组和链表才是道(结构基础)。那些多样化的数据结构,究其源头,都是在链表或数组上的特殊操作,API不同而已。

比如队列、栈这两种数据结构既可以用链表也可以用数组实现。用数组实现,就要处理扩容缩容问题;用链表实现,没有这个问题,但需要更多的内存空间存储节点指针。

〔 图〕的两种表示方式,邻接表就是链表,邻接矩阵就是二维数组。邻接矩阵判断连通性迅速,并可以进行矩阵运算解决一些问题,但是如果图比较稀疏的话很耗费空间。邻接表比较节省空间,但很多操作的效率上肯定比不过邻接矩阵。

〔散列表〕就是通过散列函数把键映射到一个大数组里。而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单,但需要额外空间存储指针;线性探查法就需要数组特性,以便连续寻址,不需要指针的存储空间,但操作稍微复杂些。(注:解决冲突常用方法有拉链法和开放地址检测法(包括线性探测、平方探测、双散列),不常用的有再散列。这里没有看出拉链法相比其他方法的简单之处?另,散列表本身即数组特性,拉链法加入了链表特性)

〔树〕,用数组实现就是〔堆〕,因为〔堆〕是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单(really?);用链表实现就是很常见的那种〔树〕,不一定是完全二叉树(则不适合数组存储)。为此,在这种链表〔树〕结构之上,又衍生出各种巧妙的设计,比如二叉搜索树、AVL树、红黑树、区间树、B树等等,以对应不同的问题

Redis提供列表、字符串、集合等几种常用的数据结构,但对于每种数据结构,底层的存储方式都至少有两种,以便根据存储数据的实际情况使用合适的存储方式。

数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且的相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,数组如果要扩容需要分配一块更大的空间,再把数据全部复制过去,时间复杂度O(N);在数组中间插入和删除每次也必须搬移后面的所有数据以保持连续,时间复杂度O(N)。

链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后继,操作指针即可删除该元素或者插入新元素,时间复杂度O(1)。但正是因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向(前)后元素位置的指针,会消耗更多的存储空间。


二、数据结构的基本操作

对于任何数据结构,基本操作无非遍历+访问,再具体一点就是:增删查改。各种数据结构的遍历+访问无非两种形式:线性的和非线性的。

线性就是for/while迭代为代表,非线性就是递归为代表。再具体一点无非以下几种框架:

1. 数组遍历框架,典型的线性迭代结构

2. 链表遍历框架,兼具迭代和递归结构:

class ListNode{

    int val;

    ListNode next;

}

void traverse(ListNode head) {

    // 迭代访问

    for(ListNode p = head; p != null; p = p.next){

    }

    // 递归访问

    while(head != null)

        traverse(head.next);

}

3.二叉树遍历框架,典型的非线性递归遍历结构:

class TreeNode {

    int val;

    TreeNode left, right;

}

void traverse(TreeNode root) {

    if(root == null) return;

    // 前序

    root.val;

    traverse(root.left);

    traverse(root.right);

}

二叉树和的递归遍历方式其实和链表的递归遍历方式很相似!它们的结构也很相似!二叉树扩展到多叉树也很类似:

class TreeNode {

    int val;

    TreeNode[] children;

}

void traverse(TreeNode root){

    // 前序

    root.val;

    for(TreeNode child : root.children)

        traverse(child);

}

N叉树的遍历又可以扩展为图的遍历,因为图就是好几棵N叉树的结合体。而对于有环图,用个布尔数组visited做标记就行了。


三、算法刷题指南

先刷二叉树

因为二叉树最容易培养框架思维,且大部分算法技巧本质上都是树的遍历问题。

leetcode124题,难度hard,求二叉树中最大路径和,主要代码如下:

int ans = INT_MIN;

int oneSideMax(TreeNode root) {

    if(root == null) return 0;

    int left = max(0, oneSideMax(root.left));

    int right = max(0, oneSideMax(root.right));

    ans = max(ans, left + right + root.val);

    return max(left + right) + root.val;

}

这就是一个后序遍历。

leetcode 105题,难度medium,根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题,主要代码如下:

TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> inMap) {

    if(preStart > preEnd || inStart > inEnd) return null;

    TreeNode root=new TreeNode(preOrder[preStart]);

    int inRoot = inMap.get(root.val);

    int numsLeft = inRoot - inStart;

    root.left = buildTree(preorder, preStart + 1, preStart +numsLeft, inorder, inStart, inRoot - 1, inMap);

    root.right = buildTree(preorder, preStart + numsLeft +1, preEnd, inorder, inRoot + 1, inEnd, inMap);

    return root;

}

不要看这个函数参数很多,只是为了控制数组索引而已,本质上也就是一个前序遍历。

leetcode 99题,难度hard,恢复一棵BST,主要代码如下:

void traverse(TreeNode node) {

    if(node == null) return;

    traverse(node.left);

    if(node.val < pre.val) {

        s = (s == null) ? prev : s;

        t = node;

    }

    prev = node;

    traverse(node.right);

}

这是一个中序遍历,一棵BST的中序遍历意味着值的升序排列。

刷完整个二叉树专题,再回去做回溯动规分治专题,会发现只要涉及递归的问题,都是树的问题。

很多动态规划问题就是在遍历一棵树,你如果对树的遍历操作烂熟于心,起码知道怎么把思路转化成代码,也知道如何提取别人解法的和新思路。

回溯算法就是个N叉树的前后序遍历问题,没有例外。比如N皇后问题,主要代码如下:

void backtrack(int[] nums, LinkedList<Integer> track) {

    if(track.size() == nums.length) {

        res.add(new LinkedList(track));

        return;

    }

    for(int i = 0; i < nums.length; i++){

        if(track.contains(nums[i])) continue;

        track.add(nums[i]);

        // 进入下一层决策树

        backtrack(nums, track);

        track.removeLast();

    }

}


// 提取出N叉树遍历框架

void backtrack(int[] nums, LinkedList<Integer> track) {

    for(int i = 0; i <nums.length; i++) {

        backtrack(nums, track);

    }

}

框架思维很重要,动态规划讲解中总结的找状态转移方程的几步流程,有时候按照流程写出解法,莫名其妙就对了。这就是框架的力量,能保证你在快睡着的时候,依然能写出正确的程序。



四、总结

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

刷算法题建议从〔树〕分类开始,结合框架思维,把这几十道题刷完,对于树的理解应该就到位了。这时候去看回溯、动态规划、分治等算法专题,对思路的理解可能会更加深刻一些。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容