【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做标记就行了。


三、算法刷题指南

先刷二叉树

【labuladong的算法小抄】0. 学习数据结构和算法的思维框架_第1张图片

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

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 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 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 track) {

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

        backtrack(nums, track);

    }

}

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



四、总结

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

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

你可能感兴趣的:(【labuladong的算法小抄】0. 学习数据结构和算法的思维框架)