数据结构 - liuzhen153/play-algorithm-python GitHub Wiki

什么是数据结构

该章大量参考并使用『我的第一本算法书』中的内容,如果感兴趣的同学可以去购买该书或该书电子版查阅。

计算机存储数据时,犹如成行成列的『箱子』,每个『箱子』存储一个数据,决定了数据存储的顺序和位置关系的便是【数据结构】

如何选择数据结构-电话薄的设计

现在大家都用手机,存储和查询电话号码十分方便。一个电话薄看似简单,这里面也能映射出选择合适的数据结构的重要性。

方式一:顺次添加

当我们有一个纸质电话薄,在得到一个新号码时,只能从上往下记录,这种方式对写一条新数据来说十分方便,例如:

name tel
Jack 123xxx
Emma 456xxx
Colin 789xxx
Leon 132xxx
ChaoChao 132xxx
... ...

假如我们要给『Tommy』打电话,在上面的排序中,我们不知道Tommy具体在哪,只能从上往下翻,胆子野一些说不定『随机查找』更快些。但是无论怎样,当这种存储很大时,我们找起电话来就不容易了。

方式二:按姓名首字母排序

无论中文名还是英文名,我们都可以找到其首字母。如果以联系人姓名首字母排序,数据都会是以字典顺序排列的,他们就是有『结构』的,例如:

name tel
ChaoChao 132xxx
Colin 789xxx
Emma 456xxx
Jack 123xxx
Leon 132xxx
... ...

这时,如果我们要给『Tommy』打电话,就可以很轻松确认Tommy的大致位置。

但是,如果我们要添加一条数据呢?比如我们新认识一个朋友『Fred』,要将他的手机号写到电话薄里。由于我们的电话薄是按照姓名首字母排序的,所以需要将其写在EmmaJack的中间,但是上面已经没有空位可以写,所以需要将Jack的『内容写到下一行,并清空本行』,相对应的,后面所有的数据都要执行『将本行内容写到下一行,并清空本行』的操作。如果一次操作耗时10s,当我们的电话薄很大时,添加一个人进来可能要花费好几个小时。

方式三:将顺序添加和按首字母排序结合

通过上面的研究,我们发现:

  • 顺序添加方式的新增速度快、成本小
  • 按首字母排序方式的查询速度快

如果我们将电话薄分成多个部分呢?我们将每一个部分记为一个表,每一个表对应一个首字母:表A、表B、 表C... 例如:

表C

name tel
ChaoChao 132xxx
Colin 789xxx
... ...

表E

name tel
Emma 456xxx
Elin 256xxx
Elfa 356xxx
... ...

表J

name tel
Jack 123xxx
Jonny 632xxx
... ...

这样一来,我们每次添加新数据就可以找到对应的表往后面顺序写就行了,而查询时也可以快速定位到大概位置。

虽说各个表中的存储还是无规律的,每次都要从表头开始往下查找,但是也要比从整个电话薄中查找要快得多了!

几种基本的数据结构

图引自『我的第一本算法书』第一章

1.链表

概念图

如下图,BlueYellowRed三个字符串存储于链表中,每个数据都有一个『指针』,它指向下一个数据的内存地址(Red是最后一个数据,其指针不指向任何位置)。

概念图

在内存中存储

由上述内容可知,在链表中,数据无需存储在连续空间内,一般都是分散存储的。

内存中存储图

访问方式 - 顺序访问

因为数据是分散存储的,所以想要访问一个数据,只能从第一个数据开始,顺着指针往下找。比如,想查找Red,查找顺序如下:

Blue -> Yellow -> Red(Done)

查找数据的耗时和链表长度n有关,时间复杂度为:

O(n)

添加数据

如果想添加数据,只需改变添加位置前后的指针指向即可,比如『在BlueYellow直接添加Green』:

添加数据

添加数据的耗时和链表长度n无关,如果已经到达需要添加数据的位置,则时间复杂度为:

O(1)

删除数据

删除数据也一样,我们只需要改变指针的指向即可,比如『删除Yellow』:

删除数据

此时,Yellow本身还存在,不过没关系,它已经无法被访问,今后需要用到其所在存储空间时,直接覆盖掉即可。

和『添加数据』一样,删除数据的时间复杂度为:

O(1)

特点

由上述内容我们可以看出,链表有以下特点:

  • 数据呈线性排列,分散存储
  • 访问比较耗时 O(n)
  • 数据的添加和删除都比较方便 O(1)

2.数组

概念图

数组也是线性排列的,它的结构和最开始讲的『按姓名首字母排序的电话薄』类似。

概念图

在内存中存储

数组的数据按顺序存放在连续空间内:

内存中存储图

访问方式 - 随机访问

因为数据存放在连续空间内,所以我们可以用数组下标计算出每个数据的内存位置,借此直接访问目标数据,这叫做『随机访问』:

数组访问-随机访问

我们可以通过数组下标查找目标数据,查找数据的耗时和数组长度n无关,时间复杂度恒为:

O(n)

添加数据

如果想添加数据,需要经过以下步骤:

① 在数组末位增加存储空间

② 从新数据要存放的位置,把后面已有数据一个个后移

③ 插入新数据

例如,我们尝试将Green添加到第二个位置上:

添加数据

添加数据的耗时和链表长度n有关,如果在数组头部添加数据,则时间复杂度为:

O(n)

删除数据

删除数据是将『添加数据』反过来操作一遍,需要经过以下步骤:

① 删除目标数据

② 把目标数据后面已有数据一个个往前移

③ 删除末尾多余空间

删除数据 删除数据

和『添加数据』一样,删除数据的时间复杂度为:

O(n)

特点

由上述内容我们可以看出,数组有以下特点:

  • 数据呈线性排列,连续存储
  • 访问快速 O(1)
  • 数据的添加和删除比较复杂 O(n)

和链表对比

通过对比,我们可以根据哪种操作频繁来决定使用哪种数据结构。

访问 添加 删除
链表
数组

3.栈

概念图

栈也是线性排列的。栈就像一摞书,新书都会被堆在最上面,取书时也只能从最上面开始取。

概念图

添加数据 - 入栈(push

往栈中添加数据的操作叫做入栈(push

添加Green

入栈1

添加Red

入栈2

取出数据 - 出栈(pop

从栈中取出数据的操作叫做出栈pop

例,我们要取出Green

① 先取出Red

出栈1

② 这时Green在顶部,才能通过出栈操作取出:

出栈2

特点

由上述内容我们可以看出,栈有以下特点:

  • 『后进先出』,Last In First Out (LIFO)
  • 添加和删除数据只能在一端进行
  • 只能访问到顶端的数据
  • 要想访问中间的数据,必须通过出栈操作将目标数据移到栈顶
  • 很适合『只需要访问最新数据』的情况

4.队列

概念图

队列也是线性排列的。队列就像排队取款的人,只能从前面往后处理,新来的人只能排在队尾。

概念图

添加数据 - 入列

往队列中添加数据的操作叫做入列

添加Green

入列1

添加Red

入列2

取出(删除)数据 - 出列

从队列中取出(删除)数据的操作叫做出列

例,我们要取出Green

① 先取出(删除)Blue

出列1

② 这时Green在头部,才能通过出列操作取出(删除):

出列2

特点

由上述内容我们可以看出,队列有以下特点:

  • 『先进先出』,First In First Out (FIFO)
  • 添加和删除数据是在两端进行的(添加-尾端,删除-头部)
  • 只能访问到头部的数据
  • 要想访问中间的数据,必须通过出列操作将目标数据移到头部
  • 很适合『先来的数据先处理』的情况

进阶-几种复杂的数据结构

埋坑,后续补充

  • 哈希
  • 二叉树