学习Algorithm思想(1)

第零章、必读系列

Posted by Haiming on March 10, 2020

看到 Github 的 trend 上面有个迅速攀升的 repo,一看是讲如何面对算法和如何解决算法的。刚好在家闲来无事,学习一下。

https://github.com/labuladong/fucking-algorithm

能看到真的说人话的教程真的很幸运,不要放弃这种学习透彻的机会。

学习数据结构和算法的框架思维

作者首先讲了,本 repo 的目的就是给数据结构和算法做一个框架性的认识。

从整体到细节,自顶向下,从抽象到具体的框架思维是通用的。

一、数据结构的存储方式

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

至于说其他的数据类型,比如树,图,堆,队列等等,都只是“上层建筑”,其最底层的结构基础还是数组和链表。因为这些多样化的数据结构,究其源头,都只是在链表和数组上面的特殊操作,只是API不同而已。

  1. ”队列“,”栈“这两种数据结构既可以使用链表也可以使用数组实现,用数组实现,就要考虑扩容缩容的问题,用链表实现,就得更多的内存空间来存储节点指针。
  2. ”图“的两种方法,邻接表就是链表,邻接矩阵就是二维数组。邻接矩阵判断连通性迅速,且可以使用邻接矩阵运算解决某些问题,但是如果图比较稀疏的话会浪费空间。邻接表就比较节省空间,但是很多操作的效率上面不如邻接矩阵。
  3. ”散列表“,就是使用散列函数将键映射到大数组之中,而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单但是需要额外的空间存储指针。线性探查法就需要数组特性,以便连续寻址,但是操作稍微复杂一些。
  4. ”树“,用数组实现就是,因为堆是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单。用链表实现就是比较常见的“树”,因为不一定是完全二叉树,所以不适合用数组存储。为此,在“链表树“的结构之上,又延伸出各种巧妙的设计,比如二叉搜索树,AVL树,红黑树,区间树,B树等,来应对不同的问题。

举个例子,对于Redis来说,其提供列表,字符串,集合等等几种常用的数据结构,但是对于每种数据结构,其底层的存储方式都至少是两种,以便根据存储数据的实际情况选择合适的存储方式。

综上所述,数据结构种类很多,甚至可以发明自己的数据结构。但是底层存储,无非是数组或者链表,二者的优缺点对比如下:

  1. 数组可以随机访问,且相对节约存储空间(无需存储额外的指针),但是正因为是连续存储,其空间最好一次性分够,所以说如果数组要扩容,必须重新分配一块额外的更大空间,再把数据全部复制过去,时间复杂度O(N)。而且想在数组的中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度O(N)。
  2. 链表因为元素不连续,所以不存在数组的扩容问题。只要知道某个元素的前驱和后驱,操作指针即可删除或者插入元素,时间复杂度为O(1)。但是也正是因为存储空间不连续,所以无法根据一个索引来算出其元素的地址,也就是不可以随机访问。而且每个元素要存储其前后的元素地址,会占用更多的存储空间。

二、数据结构的基本操作

作者将整个数据结构的操作分为两个方面进行描述:线性/非线性,递归/遍历。

对于任何的数据结构,其基本操作都是CRUD,也就是遍历+ 访问。

那么遍历+访问的方式就是分为两种:线性的和非线性的。

线性的就是以 for while 为代表,而非线性就是以递归为代表。下面几种框架是具体的形式:

数组遍历框架,线性迭代:

 void traverse(int[] arr){
        for(int i =0;i<arr.length;i++){
            //Iterate visit arr
        }
    }

链表遍历框架,兼具迭代和递归结构,下面代码之中将二者都提供:

 class ListNode {
        int val;
        ListNode next;
    }
		//迭代访问
    void traverseList(ListNode head) {
        for (ListNode p = head; p != null; p = p.next) {
            //Iterator visit LinkedList
        }
    }
		//递归访问
    void traverseList(ListNode head) {
        traverseList(head.next);
    }

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

 class TreeNode{
        int val;
        TreeNode left;
        TreeNode right;
    }
    
    void traverseTree(TreeNode root){
        traverseTree(root.left);
        traverseTree(root.right);
    }

在递归遍历结构之中,大家都是相似的。从二叉树扩展到多叉树也是一样的步骤:

    class TreeNode{
        int val;
        TreeNode[] child;
    }

    void traverseTree(TreeNode root){
        for(TreeNode treeNode:root.child){
            traverseTree(treeNode);
        }
    }

从多叉树扩展到图的遍历也是一样的步骤,因为图就是几棵树的结合体。如果防止出现坏图的情况,只要使用一个布尔数组做标记就可以了。

三、算法刷题指南

先刷二叉树