数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。这句话怎么理解,不是还有散列表、栈、队列、堆、树、图等等各种数据结构吗?

一、数据结构的存储办法

数据结构的存储办法只需两种:数组(次序存储)和链表(链式存储)。

这句话怎样了解,不是还有散列表、栈、行列、堆、树、图等等各种数据结构吗?

咱们剖析问题,必定要有递归的思想,自顶向下,从笼统到详细。你上来就列出这么多,那些都归于「上层建筑」,而数组和链表才是「结构根底」。拜访那些多样化的数据结构,究其源头,都是在链表或许数组上的特别操作,API 不同罢了。

比方说「行列」、「栈」这两种数据结构既可以运用链表也可以运用数组完成。用数组完成,就要处理扩容缩容的问题;用链表完成,没有这个问题,但需求更多的内存空间存储节点指针。

「图」的两种表明办法,邻接表便是链表,邻接矩阵便是二维数组。邻接矩阵判别连通性敏捷,并可以进行矩阵运算处理一些问题,可是假如图比较稀少的话很耗费空间。邻接表比较节省空间,可是许多操作的功率上必定比不过邻接矩阵。

「散列表」便是经过散列函数把键映射到一个大数组里。并且关于处理散列抵触的办法,拉链法需求链表特性,操作简略,但需求额定的空间存储指针;线性探查法就需求数组特性,以便接连寻址,不需求指针的存储空间,但操作略微杂乱些。

「树」,用数组完成便是「堆」,拜访「堆」是一个彻底二叉树,用数组存储不需求节点指针,操作也比较简略;用链表完成便是很常见的那种「树」,拜访不必定是彻底二叉树,所以不适合用数组存储。为此,在这种链表「树」结构之上,又衍生出各种奇妙的规划,比方二叉查找树、AVL 树、红黑树、区间树、B 树等等,以应对不同的问题。

了解 Redis 数据库的朋友或许也知道,Redis 供给列表、字符串、调集等等几种常用数据结构,可是关于每种数据结构,底层的存储办法都至少有两种,以便于依据存储数据的实际情况运用适宜的存储办法。

综上,数据结构品种许多,乃至你也可以创造自己的数据结构,可是底层存储无非数组或许链表,二者的优缺点如下:

数组由所以紧凑接连存储,可以随机拜访,经过索引快速找到对应元素,并且相对节省存储空间。但正拜访接连存储,内存空间有必要一次性分配够,所以说数组假如要扩容,需求重新分配一块更大的空间,再把数据悉数仿制曩昔,时刻杂乱度 O(N);并且你假如想在数组中心进行刺进和删去,每次有必要搬移后边的一切数据以坚持接连,时刻杂乱度 O(N)。

链表拜访元素不接连,而是靠指针指向下一个元素的方位,所以不存在数组的扩容问题;假如知道某一元素的前驱和后驱,操作指针即可删去该元素或许刺进新元素,时刻杂乱度 O(1)。可是正拜访存储空间不接连,你无法依据一个索引算出对应元素的地址,所以不能随机拜访;并且拜访每个元素有必要存储指向前后元素方位的指针,会耗费相对更多的贮存空间。

二、数据结构的底子操作

关于任何数据结构,其底子操作无非遍历 + 拜访,再详细一点便是:增删查改。

数据结构品种许多,但它们存在的意图都是在不同的使用场景,尽或许高效地增删查改。话说这不便是数据结构的任务么?

怎样遍历 + 拜访?咱们仍然从最高层来看,各种数据结构的遍历 + 拜访无非两种办法:线性的和非线性的。

线性便是 for/while 迭代为代表,非线性便是递归为代表。再详细一步,无非以下几种结构:

数组遍历结构,典型的线性迭代结构:

  1. voidtraverse(int[]arr){for(inti=0;i<arr.length;i++){//迭代拜访arr[i]}}

链表遍历结构,兼具迭代和递归结构:

  1. /*底子的单链表节点*/classListNode{intval;ListNodenext;}
  2. voidtraverse(ListNodehead){for(ListNodep=head;p!=null;p=p.next){//迭代拜访p.val}}
  3. voidtraverse(ListNodehead){//递归拜访head.valtraverse(head.next);}

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

  1. /*底子的二叉树节点*/classTreeNode{intval;TreeNodeleft,right;}
  2. voidtraverse(TreeNoderoot){traverse(root.left);traverse(root.right);}

你看二叉树的递归遍历办法和链表的递归遍历办法,类似不?再看看二叉树结构和单链表结构,类似不?假如再多几条叉,N 叉树你会不会遍历?

二叉树结构可以扩展为 N 叉树的遍历结构:

  1. /*底子的N叉树节点*/classTreeNode{intval;TreeNode[]children;}
  2. voidtraverse(TreeNoderoot){for(TreeNodechild:root.children)traverse(child);}

N 叉树的遍历又可以扩展为图的遍历,拜访图便是好几 N 叉棵树的结合体。你说图是或许呈现环的?这个很好办,用个布尔数组 visited 做符号就行了,这儿就不写代码了。

所谓结构,便是套路。不论增删查改,这些代码都是永久无法脱离的结构,你可以把这个结构作为纲要,依据详细问题在结构上增加代码就行了,下面会详细举例。

三、算法刷题攻略

首先要清晰的是,数据结构是东西,算法是经过适宜的东西处理特定问题的办法。也便是说,学习算法之前,最最少得了解那些常用的数据结构,了解它们的特性和缺点。

那么该怎样在 LeetCode 刷题呢?之前的文章算法学习之路写过一些,什么按标签刷,坚持下去如此。现在距那篇文章现已曩昔将近一年了,我不说那些不痛不痒的话,直接说详细的主张:

  • 先刷二叉树,先刷二叉树,先刷二叉树!

这是我这刷题一年的亲自领会,下图是上一年十月份的提交截图:

 懂了数据结构结构思想,抚育算法不过是纸老虎 数据结构 算法 分析 第1张

大众号文章的阅览数据显现,大部分人对数据结构相关的算法文章不感兴趣,而是更关心动规回溯分治等等技巧。为什么要先刷二叉树呢,拜访二叉树是最简单培育结构思想的,并且大部分算法技巧,本质上都是树的遍历问题。

刷二叉树看到标题没思路?依据许多读者的问题,其实咱们不是没思路,仅仅没有了解咱们说的「结构」是什么。不要小看这几行破代码,简直一切二叉树的标题都是一套这个结构就出来了。

  1. voidtraverse(TreeNoderoot){//前序遍历traverse(root.left)//中序遍历traverse(root.right)//后序遍历}

比方说我随意拿几道题的解法出来,不必管详细的代码逻辑,只需看看结构在其间是怎样发挥作用的就行。

LeetCode 124 题,难度 Hard,让你求二叉树中最大途径和,首要代码如下:

  1. intans=INT_MIN;intoneSideMax(TreeNode*root){if(root==
  2. nullptr)return0;intleft=max(0,oneSideMax(root->left));
  3. intright=max(0,oneSideMax(root->right));ans=max(ans,left
  4. +right+root->val);returnmax(left,right)+root->val;}

你看,这便是个后序遍历嘛。

LeetCode 105 题,难度 Medium,让你依据前序遍历和中序遍历的成果复原一棵二叉树,很经典的问题吧,首要代码如下:

  1. TreeNodebuildTree(int[]preorder,intpreStart,intpreEnd,int[]inorder,intinStart,intinEnd,Map<Integer,Integer>inMap){
  2. if(preStart>preEnd||inStart>inEnd)returnnull;
  3. TreeNoderoot=newTreeNode(preorder[preStart]);intinRoot=inMap.get(root.val);intnumsLeft=inRoot-inStart;
  4. 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);returnroot;}

不要看这个函数的参数许多,仅仅为了操控数组索引罢了,本质上该算法也便是一个前序遍历。

LeetCode 99 题,难度 Hard,康复一棵 BST,首要代码如下:

  1. voidtraverse(TreeNode*node){if(!node)return;
  2. traverse(node->left);if(node->val<prev->val){s=(s
  3. ==NULL)?prev:s;t=node;}prev=node;
  4. traverse(node->right);}

这不便是个中序遍历嘛,关于一棵 BST 中序遍历意味着什么,应该不需求说明了吧。

你看,Hard 难度的标题不过如此,并且还这么有规则可循,只需把结构写出来,然后往相应的方位加东西就行了,这不便是思路吗。

关于一个了解二叉树的人来说,刷一道二叉树的标题花不了多长时刻。那么假如你对刷题无从下手或许有害怕心思,无妨从二叉树下手,前 10 道或许有点难过;结合结构再做 20 道,或许你就有点自己的了解了;刷完整个专题,再去做什么回溯动规分治专题,你就会发现只需触及递归的问题,都是树的问题。

再举例吧,说几道咱们之前文章写过的问题。

动态规划详说明过凑零钱问题,暴力解法便是遍历一棵 N 叉树:

 懂了数据结构结构思想,抚育算法不过是纸老虎 数据结构 算法 分析 第2张

  1. defcoinChange(coins:List[int],amount:int):
  2. defdp(n):ifn==0:return0ifn<0:return-1
  3. res=float('INF')forcoinincoins:subproblem=dp(n-coin)#子问题无解,越过ifsubproblem==-1:continueres=min(res,1+subproblem)returnresifres!=float('INF')else-1
  4. returndp(amount)

这么多代码看不懂咋办?直接提取出结构,就能看出中心思路了:

  1. #不过是一个N叉树的遍历问题罢了defdp(n):forcoinincoins:dp(n-coin)

其实许多动态规划问题便是在遍历一棵树,你假如对树的遍历操作纯熟于心,最少知道怎样把思路转化成代码,也知道怎样提取他人解法的中心思路。

再看看回溯算法,前文回溯算法详解爽性直接说了,回溯算法便是个 N 叉树的前后序遍历问题,没有破例。

比方 N 皇后问题吧,首要代码如下:

  1. /*底子的单链表节点*/classListNode{intval;ListNodenext;}
  2. voidtraverse(ListNodehead){for(ListNodep=head;p!=null;p=p.next){//迭代拜访p.val}}
  3. voidtraverse(ListNodehead){//递归拜访head.valtraverse(head.next);}
0

N 叉树的遍历结构,找出来了把~你说,树这种结构重不重要?

综上,关于害怕算法的朋友来说,可以先刷树的相关标题,试着从结构上看问题,而不要纠结于细节问题。

纠结细节问题,就比方纠结 i 究竟应该加到 n 仍是加到 n - 1,这个数组的巨细究竟应该开 n 仍是 n + 1 ?

从结构上看问题,便是像咱们这样根据结构进行抽取和扩展,既可以在看他人解法时快速了解中心逻辑,也有助于找到咱们自己写解法时的思路方向。

当然,假如细节犯错,你得不到正确的答案,可是只需有结构,你再错也错不到哪去,拜访你的方向是对的。

可是,你要是心中没有结构,那么你底子无法解题,给了你答案,你也不会发现这便是个树的遍历问题。

这种思想是很重要的,动态规划详解中总结的找状况搬运方程的几步流程,有时分依照流程写出解法,说实话我自己都不知道为啥是对的,横竖它便是对了。。。

这便是结构的力气,可以确保你在快睡着的时分,仍然能写出正确的程序;就算你啥都不会,都能比他人高一个等级。

四、总结几句

数据结构的底子存储办法便是链式和次序两种,底子操作便是增删查改,遍历办法无非迭代和递归。

刷算法题主张从「树」分类开端刷,结合结构思想,把这几十道题刷完,关于树结构的了解应该就到位了。这时分去看回溯、动规、分治等算法专题,对思路的了解或许会愈加深入一些。

转载请说明出处
知优网 » 懂了数据结构结构思想,抚育算法不过是纸老虎

发表评论

您需要后才能发表评论