【内功修炼系列1】线性数据结构(上篇)

1,741

故事的开始

最近和一些刚毕业两三年的同事聊天,发现多数同学都忙于业务的开发,对于自己日常开发的工具和CRUD得心应手。而且对于上面安排下来的开发任务都可以胜任,对于应用层面已经非常熟练,可以说是非常合格的“CRUD BOY”。但是一聊到基础的东西反而显得有点迟疑和生涩,也就是常说的基本功不扎实。一旦遇到比较复杂甚至难的问题时,很容易挠头懵逼。

以我的个人经验来讲,软件开发工程师相当于武侠小说中的人物,如果想达到更牛逼的境界成为武林高手,那么“内功深厚”必须是基本前提。软件开发所用到的所有技术,相当于各种招式,例如:微服务、大数据等,都是为了解决一些场景问题而产生的技术。那么如果想用的好,基本离不开基础的支撑。所以说,当你学习了一段时间之后,就要回头看看基础的东西,基础掌握的越深,你对一些技术的了解也就越深。举个例子,如果你想用好微服务的注册中心,那你就要了解服务注册发现的原理,要了解服务间通信的模式(RPC或HTTP)。也要了解CAP是什么,要了解注册中心集群的选举算法(如:raft)等等。

那么作为java开发,你要了解:数据结构、算法、JVM内存模型、并发编程、GC原理等等。这些基础东西在日常开发也许不会用到,因为java本身已经提供了很多的封装,甚至网上一些开源工具已经帮你封装好了。但是你想往中高级开发的方向走,我提到的这些内容都是你的必修课。你也许在日常开发中能完成任务,代码写的差一点也能跑。但是当你去面试的时候,这些问题一旦问到你,回答不出来?不好意思,GG。因为这些内容对于面试官来讲,是一定会问到的。例如:当问到并发编程时,可重入锁是如何实现的?CLH队列是干什么的?CAS是什么?这些东西都是互联网产品并发一定会用到的技术。

最近自己在回顾这些基础内容的同时,写一些文章当做存底,以供日后回看。另一方面也希望我在回顾的同时,也能帮助到一些忽略了这些内容的同学,成为一名合格的程序员。所以想出一个“内功修炼系列”文章,每个主题分为上下两篇,上篇主要介绍概念、讲清楚用处。下篇主要是针对上期做一些代码实现、常见面试时的上机代码实践,毕竟实践出真知。同时和大家一起讨论成长,跟大家成为朋友。甚至有些写的不妥之处,希望大家在留言区或者加我微信直接怼我。

进入主题

那么第一篇咱们就一起看看常用的线性数据结构:数组、链表、栈、队列、哈希表。

数组

数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。

我们先来看一张图(PS:图都是用PPT画的,比较丑陋,先看着)

上面是创建了一个长度为6的数组,右边表格的内容,是模拟jvm的内存地址。数组有一下特点:

  • 长度是确定的。数组一旦被创建,它的大小就是不可以改变的;
  • 其元素必须是相同类型,不允许出现混合类型;
  • 数组类型可以是任何数据类型,包括基本类型和引用类型;
  • 数组的元素在堆内存中被分配空间,并且是连续分配的;
  • 数组的元素都是有序号的,序号从0开始;

那我们初始化好数组后,分配了连续的内容,我们通过数组序号取对应值非常方便,可以随意通过序号获取对应数组元素的值。而且时间复杂度是o(1),也就是说通过一次查询就可以拿到数据对应的元素值。接下来我们再看看另外的操作:插入和删除。

当我们要向数组第三个位置插入“主任”的时候,原来下标为2的元素“患者”,以及后面的元素,都要移动并重新赋值每个元素。删除是同样的道理,当要删除“患者”的时候,其后面的元素都要移动并重新复制下标。从这一特点来看,插入和删除并不像查询那样高效,并且插入和删除的时间复杂度是o(n)。这样我们就可以知道,如果是频繁插入和删除的场景下,数组就并不那么适合了,尤其在数据量比较大的数组。

链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

链表分为:单向链表、双向链表。

单链表(单向链表):由两部分组成 数据域(Data)和结点域(Node)。这样原理的实现是通过Node结点区的头指针head实现的,每个结点都有一个指针,每个节点指针的指向都是指向自身结点的下一个结点,最后一个结点的head指向为null。

双链表(双向链表):双链表和单链表相比,多了一个指向尾指针(tail),双链表的每个结点都有一个头指针head和尾指针tail,双链表相比单链表更容易操作,双链表结点的首结点的head指向为null,tail指向下一个节点的tail;尾结点的head指向前一个结点的head,tail 指向为null,是双向的关系;

在单链表中若需要查找某一个元素时,都必须从第一个元素开始进行查找,查询的次数与节点数密切相关,这样单链表的查询时间复杂度就是o(n),而对于插入来讲,只需要将新加入的节点的next指向下一个节点,前面的节点的next指向新增的节点;

删除是同样一个道理,只要把要删除的节点前面的节点的next指向,要删除节点的下一个节点就可以了。在这里可以看出链表的插入和删除都只需要两部操作就可以完成,跟节点的数量无关,时间复杂度也就o(1)。这样就可以看出,链表数据结构更适合插入和删除多的场景,效率明显比数组要强很多。

栈和队列

栈是一种线性数据结构(FILO),栈的特征是数据的插入和删除只能通过一端来实现,这一端称为“栈顶”,相应的另一端称为“栈底”。

队列也是一种线性数据结构(FIFO),特殊之处在于它只允许在前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受到限制的线性表。进行插入操作的端称为“队尾”,进行删除操作的端称为“队头”。

栈和队列也是我们日常开发最为常见的数据结构,利用它们本身的特性可以实现很多功能。栈的先进后出特性,我们可以在一些匹配场景下使用。例如:匹配括号,两两配对等。队列的先进先出的特性,最为常见的就是我们的消息队列,多线程的双端队列等。与链表一样,栈和队列的查询复杂度是o(n),插入和删除操作是0(1),大家可以根据适合场景选择。

哈希表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

在使用hash表的时候,通常是先对key进行hash, 就是把输入值“压缩”并转成更小的值,这个值通常状况下是唯一、格式极其紧凑的。如图(网上盗图):lies这个单词为key,value为20,经过hash算法之后,将key的每一个字母的ascii码求和并与内存地址30长度求模,之后算出下标是9,那么就把value放到第九个元素下。那么即使下次求值,也可直接通过key取到value。而且时间复杂度为o(1)。

说起来可能感觉有点复杂,我想我举个例子你就会明白了,最典型的例子就是字典。如果我想要查“董”字详细信息,我肯定会去根据拼音dong去查找拼音索引,首先去查dong在字典的位置,查了一下得到“董”。这过程就是键码映射,在公式里面,就是通过key去查找f(key)。其中,dong就是关键字(key),查到的页码就是哈希值,value就是董的详细信息。

哈希冲突

如图:key为“foes”时,hash值也是429,那么这时候就会出现hash值相等的问题,也就是哈希冲突。解决方式:链式地址法。链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入到该空间。我感觉业界上用的最多的就是链地址法。如图“foes”会加入链表后面,“lies”的next指向“foes”节点。hash表也是java开发过程中出镜率最高的数据结构,而且java已经封装好了HashSet、HashMap等,都可以很快的让大家使用起来。

最后

今天跟大家一起简单回顾了开发中的常用数据结构:数组、链表、栈、队列、哈希表。下篇会根据本篇的内容,举几个实际应用的场景,并通过代码实现出来。希望大家继续关注支持!让我们一起进步吧。

特别声明

本文为原创文章,如需转载可与我联系,标明出处。谢谢!

往期文章:

《【内功修炼系列1】线性数据结构(下篇1)》

《【内功修炼系列1】线性数据结构(下篇2)》