zoukankan      html  css  js  c++  java
  • 常见数据结构

    1. 什么是数据结构

    5c651a306c0f4

        数据存储于计算机的内存中。内存如右图所示,形似排成1列的箱子,1个箱子里存储1个数据。
    

    {%}

    数据存储于内存时,决定了数据顺序和位置关系的便是“数据结构”。

    2. 常用数据结构

    2.1 链表

         链表是数据结构之一,其中的数据呈线性排列。在链表中,数据的添加和删除都较为方便,就是访问比较耗费时间
    

    {90%}

    这就是链表的概念图。Blue、Yellow、Red 这3个字符串作为数据被存储于链表中。每个数据都有1个“指针”,它指向下一个数据的内存地址。

    {55%}

    在链表中,数据一般都是分散存储于内存中的,无须存储在连续空间内。

    {55%}

    因为数据都是分散存储的,所以如果想要访问数据,只能从第1个数据开始,顺着指针的指向一一往下访问(这便是顺序访问)。比如,想要找到 Red 这一数据,就得从 Blue 开始访问。

    {55%}

    这之后,还要经过 Yellow,我们才能找到 Red。

    {55%}

    如果想要添加数据,只需要改变添加位置前后的指针指向就可以,非常简单。比如,在 Blue 和 Yellow 之间添加 Green。

    {55%}

    将 Blue 的指针指向的位置变成 Green,然后再把 Green 的指针指向 Yellow,数据的添加就大功告成了。

    {55%}

    数据的删除也一样,只要改变指针的指向就可以,比如删除 Yellow。

    {75%}

    这时,只需要把 Green 指针指向的位置从 Yellow 变成 Red,删除就完成了。虽然 Yellow 本身还存储在内存中,但是不管从哪里都无法访问这个数据,所以也就没有特意去删除它的必要了。今后需要用到 Yellow 所在的存储空间时,只要用新数据覆盖掉就可以了。

    补充说明

    上文中讲述的链表是最基本的一种链表。除此之外,还存在几种扩展方便的链表。

    虽然上文中提到的链表在尾部没有指针,但我们也可以在链表尾部使用指针,并且让它指向链表头部的数据,将链表变成环形。这便是“循环链表”,也叫“环形链表”。循环链表没有头和尾的概念。想要保存数量固定的最新数据时通常会使用这种链表。

    {55%}

    另外,上文链表里的每个数据都只有一个指针,但我们可以把指针设定为两个,并且让它们分别指向前后数据,这就是“双向链表”。使用这种链表,不仅可以从前往后,还可以从后往前遍历数据,十分方便。

    但是,双向链表存在两个缺点:一是指针数的增加会导致存储空间需求增加;二是添加和删除数据时需要改变更多指针的指向。

    {55%}

    2.2 数组

        数组也是数据呈线性排列的一种数据结构。与前一节中的链表不同,在数组中,访问数据十分简单,而添加和删除数据比较耗工夫。
    

    {90%}

    这就是数组的概念图。Blue、Yellow、Red 作为数据存储在数组中。

    {50%}

    数据按顺序存储在内存的连续空间内。

    {55%}

    由于数据是存储在连续空间内的,所以每个数据的内存地址(在内存上的位置)都可以通过数组下标算出,我们也就可以借此直接访问目标数据(这叫作“随机访问”)。

    {55%}

    比如现在我们想要访问 Red。如果使用指针就只能从头开始查找,但在数组中,只需要指定a[2],便能直接访问 Red。

    {55%}

    但是,如果想在任意位置上添加或者删除数据,数组的操作就要比链表复杂多了。这里我们尝试将 Green 添加到第2个位置上。

    {60%}

    首先,在数组的末尾确保需要增加的存储空间。

    {60%}

    为了给新数据腾出位置,要把已有数据一个个移开。首先把 Red 往后移。

    {60%}

    然后把 Yellow 往后移。

    {60%}

    最后在空出来的位置上写入 Green。

    {85%}

    添加数据的操作就完成了。

    {60%}

    反过来,如果想要删除 Green……

    {60%}

    首先,删掉目标数据(在这里指 Green)。

    {60%}

    然后把后面的数据一个个往空位移。先把 Yellow 往前移。

    {60%}

    接下来移动 Red。

    {80%}

    最后再删掉多余的空间。这样一来 Green 便被删掉了。

    补充说明

    在链表和数组中,数据都是线性地排成一列。在链表中访问数据较为复杂,添加和删除数据较为简单;而在数组中访问数据比较简单,添加和删除数据却比较复杂。

    我们可以根据哪种操作较为频繁来决定使用哪种数据结构。

    访问 添加 删除
    链表
    数组

    2.3 栈

        栈也是一种数据呈线性排列的数据结构,不过在这种结构中,我们只能访问最新添加的数据。栈就像是一摞书,拿到新书时我们会把它放在书堆的最上面,取书时也只能从最上面的新书开始取。
    

    {80%}

    这就是栈的概念图。现在存储在栈中的只有数据 Blue。

    {55%}

    然后,栈中添加了数据 Green。

    {52%}

    接下来,数据 Red 入栈。

    {60%}

    从栈中取出数据时,是从最上面,也就是最新的数据开始取出的。这里取出的是 Red。

    {60%}

    如果再进行一次出栈操作,取出的就是 Green 了。

    解说

    像栈这种最后添加的数据最先被取出,即“后进先出”的结构,我们称为 Last In First Out,简称 LIFO。

    与链表和数组一样,栈的数据也是线性排列,但在栈中,添加和删除数据的操作只能在一端进行,访问数据也只能访问到顶端的数据。想要访问中间的数据时,就必须通过出栈操作将目标数据移到栈顶才行。

    应用示例

    栈只能在一端操作这一点看起来似乎十分不便,但在只需要访问最新数据时,使用它就比较方便了。

    比如,规定(AB(C(DE)F)(G((H)I J)K))这一串字符中括号的处理方式如下:首先从左边开始读取字符,读到左括号就将其入栈,读到右括号就将栈顶的左括号出栈。此时,出栈的左括号便与当前读取的右括号相匹配。通过这种处理方式,我们就能得知配对括号的具体位置。

    2.4 队列

        与前面提到的数据结构相同,队列中的数据也呈线性排列。虽然与栈有些相似,但队列中添加和删除数据的操作分别是在两端进行的。就和“队列”这个名字一样,把它想象成排成一队的人更容易理解。在队列中,处理总是从第一名开始往后进行,而新来的人只能排在队尾。
    

    {85%}

    这就是队列的概念图。现在队列中只有数据 Blue。

    {55%}

    然后,队列中添加了数据 Green。

    {50%}

    紧接着,数据 Red 也入队了。

    {55%}

    从队列中取出(删除)数据时,是从最下面,也就是最早入队的数据开始的。这里取出的是 Blue。

    {55%}

    如果再进行一次出队操作,取出的就是 Green 了。

    解说

    像队列这种最先进去的数据最先被取来,即“先进先出”的结构,我们称为 First In First Out,简称 FIFO。

    与栈类似,队列中可以操作数据的位置也有一定的限制。在栈中,数据的添加和删除都在同一端进行,而在队列中则分别是在两端进行的。队列也不能直接访问位于中间的数据,必须通过出队操作将目标数据变成首位后才能访问。

    2.5 哈希表

        在哈希表这种数据结构中,使用将在5-3节讲解的“哈希函数”,可以使数据的查询效率得到显著提升。
    

    {60%}

    哈希表存储的是由键(key)和值(value)组成的数据。例如,我们将每个人的性别作为数据进行存储,键为人名,值为对应的性别。

    {%}

    为了和哈希表进行对比,我们先将这些数据存储在数组中。

    {48%}

    此处准备了6个箱子(即长度为6的数组)来存储数据。假设我们需要查询 Ally 的性别,由于不知道 Ally 的数据存储在哪个箱子里,所以只能从头开始查询。这个操作便叫作“线性查找”。

    提示 一般来说,我们可以把键当成数据的标识符,把值当成数据的内容。

    {50%}

    0号箱子中存储的键是 Joe 而不是 Ally。

    {50%}

    1号箱子中的也不是 Ally。

    {50%}

    同样,2号、3号箱子中的也都不是 Ally。

    {50%}

    查找到4号箱子的时候,发现其中数据的键为 Ally。把键对应的值取出,我们就知道 Ally 的性别为女(F)了。

    {50%}

    数据量越多,线性查找耗费的时间就越长。由此可知:由于数据的查询较为耗时,所以此处并不适合使用数组来存储数据。

    {50%}

    但使用哈希表便可以解决这个问题。首先准备好数组,这次我们用5个箱子的数组来存储数据。

    {50%}

    尝试把 Joe 存进去。

    {50%}

    使用哈希函数(Hash)计算 Joe 的键,也就是字符串“Joe”的哈希值。得到的结果为4928。

    {55%}

    将得到的哈希值除以数组的长度5,求得其余数。这样的求余运算叫作“mod 运算”。此处 mod 运算的结果为3。

    {55%}

    因此,我们将 Joe 的数据存进数组的3号箱子中。重复前面的操作,将其他数据也存进数组中。

    {55%}

    Sue 键的哈希值为7291,mod 5的结果为1,将 Sue 的数据存进1号箱中。

    {55%}

    Dan 键的哈希值为1539,mod 5的结果为4,将 Dan 的数据存进4号箱中。

    {80%}

    Nell 键的哈希值为6276,mod 5的结果为1。本应将其存进数组的1号箱中,但此时1号箱中已经存储了 Sue 的数据。这种存储位置重复了的情况便叫作“冲突”。

    {55%}

    遇到这种情况,可使用链表在已有数据的后面继续存储新的数据。

    {55%}

    Ally 键的哈希值为9143,mod 5的结果为3。本应将其存储在数组的3号箱中,但3号箱中已经有了 Joe 的数据,所以使用链表,在其后面存储 Ally 的数据。

    {55%}

    Bob 键的哈希值为5278,mod 5的结果为3。本应将其存储在数组的3号箱中,但3号箱中已经有了 Joe 和 Ally 的数据,所以使用链表,在 Ally 的后面继续存储 Bob 的数据。

    {55%}

    像这样存储完所有数据,哈希表也就制作完成了。

    {55%}

    接下来讲解数据的查询方法。假设我们要查询 Dan 的性别。

    {55%}

    为了知道 Dan 存储在哪个箱子里,首先需要算出 Dan 键的哈希值,然后对其进行 mod 运算。最后得到的结果为4,于是我们知道了它存储在4号箱中。

    {55%}

    查看4号箱可知,其中的数据的键与 Dan 一致,于是取出对应的值。由此我们便知道了 Dan 的性别为男(M)。

    {55%}

    那么,想要查询 Ally 的性别时该怎么做呢?为了找到它的存储位置,先要算出 Ally 键的哈希值,再对其进行 mod 运算。最终得到的结果为3。

    {55%}

    然而3号箱中数据的键是 Joe 而不是 Ally。此时便需要对 Joe 所在的链表进行线性查找。

    {55%}

    于是我们找到了键为 Ally 的数据。取出其对应的值,便知道了 Ally 的性别为女(F)。

    解说

    在哈希表中,我们可以利用哈希函数快速访问到数组中的目标数据。如果发生哈希冲突,就使用链表进行存储。这样一来,不管数据量为多少,我们都能够灵活应对。

    如果数组的空间太小,使用哈希表的时候就容易发生冲突,线性查找的使用频率也会更高;反过来,如果数组的空间太大,就会出现很多空箱子,造成内存的浪费。因此,给数组设定合适的空间非常重要。

    补充说明

    在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突。这种方法被称为“链地址法”。

    除了链地址法以外,还有几种解决冲突的方法。其中,应用较为广泛的是“开放地址法”。这种方法是指当冲突发生时,立刻计算出一个候补地址(数组上的位置)并将数据存进去。如果仍然有冲突,便继续计算下一个候补地址,直到有空地址为止。可以通过多次使用哈希函数或“线性探测法”等方法计算候补地址。

    2.6 堆

        堆是一种图的树形结构,被用于实现“优先队列”(priority queues)优先队列是一种数据结构,可以自由添加数据,但取出数据时要从最小值开始按顺序取出。在堆的树形结构中,各个顶点被称为“结点”(node),数据就存储在这些结点中。
    

    {75%}

    这就是堆的示例。结点内的数字就是存储的数据。堆中的每个结点最多有两个子结点。树的形状取决于数据的个数。另外,结点的排列顺序为从上到下,同一行里则为从左到右。

    {75%}

    在堆中存储数据时必须遵守这样一条规则:子结点必定大于父结点。因此,最小值被存储在顶端的根结点中。往堆中添加数据时,为了遵守这条规则,一般会把新数据放在最下面一行靠左的位置。当最下面一行里没有多余空间时,就再往下另起一行,把数据加在这一行的最左端。

    {75%}

    我们试试往堆里添加数字5。

    {60%}

    首先按照02的说明寻找新数据的位置。该图中最下面一排空着一个位置,所以将数据加在此处。

    {65%}

    如果父结点大于子结点,则不符合上文提到的规则,因此需要交换父子结点的位置。

    {65%}

    这里由于父结点的6大于子结点的5,所以交换了这两个数字。重复这样的操作直到数据都符合规则,不再需要交换为止。

    {65%}

    现在,父结点的1小于子结点的5,父结点的数字更小,所以不再交换。

    {60%}

    这样,往堆中添加数据的操作就完成了。

    {60%}

    从堆中取出数据时,取出的是最上面的数据。这样,堆中就能始终保持最上面的数据最小。

    {65%}

    由于最上面的数据被取出,因此堆的结构也需要重新调整。

    {65%}

    按照{2%}中说明的排列顺序,将最后的数据(此处为6)移动到最顶端。

    {%}

    如果子结点的数字小于父结点的,就将父结点与其左右两个子结点中较小的一个进行交换。

    {%}

    这里由于父结点的6大于子结点(右)的5大于子结点(左)的3,所以将左边的子结点与父结点进行交换。重复这个操作直到数据都符合规则,不再需要交换为止。

    {55%}

    现在,子结点(右)的8大于父结点的6大于子结点(左)的4,需要将左边的子结点与父结点进行交换。

    {55%}

    这样,从堆中取出数据的操作便完成了。

    2.7 二叉查找树

        二叉查找树(又叫作二叉搜索树或二叉排序树)是一种数据结构,采用了图的树形结构)。数据存储于二叉查找树的各个结点中。
    

    {75%}

    这就是二叉查找树的示例。结点中的数字便是存储的数据。此处以不存在相同数字为前提进行说明。

    {55%}

    二叉查找树有两个性质。第一个是每个结点的值均大于其左子树上任意一个结点的值。比如结点9大于其左子树上的3和8。

    {55%}

    同样,结点15也大于其左子树上任意一个结点的值。

    {60%}

    第二个是每个结点的值均小于其右子树上任意一个结点的值。比如结点15小于其右子树上的23、17和28。

    {55%}

    根据这两个性质可以得到以下结论。首先,二叉查找树的最小结点要从顶端开始,往其左下的末端寻找。此处最小值为3。

    {55%}

    反过来,二叉查找树的最大结点要从顶端开始,往其右下的末端寻找。此处最大值为28。

    {55%}

    下面我们来试着往二叉查找树中添加数据。比如添加数字1。

    {80%}

    首先,从二叉查找树的顶端结点开始寻找添加数字的位置。将想要添加的1与该结点中的值进行比较,小于它则往左移,大于它则往右移。

    {55%}

    由于1<9,所以将1往左移。

    {55%}

    由于1<3,所以继续将1往左移,但前面已经没有结点了,所以把1作为新结点添加到左下方。

    {55%}

    这样,1的添加操作便完成了。

    {55%}

    接下来,我们再试试添加数字4。

    {80%}

    和前面的步骤一样,首先从二叉查找树的顶端结点开始寻找添加数字的位置。

    {60%}

    由于4<9,所以将其往左移。

    {60%}

    由于4>3,所以将其往右移。

    {55%}

    由于4<8,所以需要将其往左移,但前面已经没有结点了,所以把4作为新结点添加到左下方。

    {55%}

    于是4的添加操作也完成了。

    {55%}

    接下来看看如何在二叉查找树中删除结点。比如我们来试试删除结点28。

    {55%}

    如果需要删除的结点没有子结点,直接删掉该结点即可。

    {55%}

    再试试删除结点8。

    {55%}

    如果需要删除的结点只有一个子结点,那么先删掉目标结点……

    {60%}

    然后把子结点移到被删除结点的位置上即可。

    {60%}

    最后来试试删除结点9。

    {60%}

    如果需要删除的结点有两个子结点,那么先删掉目标结点……

    {60%}

    然后在被删除结点的左子树中寻找最大结点……

    {80%}

    最后将最大结点移到被删除结点的位置上。这样一来,就能在满足二叉查找树性质的前提下删除结点了。如果需要移动的结点(此处为4)还有子结点,就递归执行前面的操作。

    {55%}

    下面来看看如何在二叉查找树中查找结点。比如我们来试试查找12。

    {55%}

    从二叉查找树的顶端结点开始往下查找。和添加数据时一样,把12和结点中的值进行比较,小于该结点的值则往左移,大于则往右移。

    提示 删除9的时候,我们将“左子树中的最大结点”移动到了删除结点的位置上,但是根据二叉查找树的性质可知,移动“右子树中的最小结点”也没有问题。

    enter image description here

    由于12>4,所以往右移。

    {60%}

    找到结点12了。

    补充说明

    有很多以二叉查找树为基础扩展的数据结构,比如“平衡二叉查找树”。这种数据结构可以修正形状不均衡的树,让其始终保持均衡形态,以提高查找效率。

    Stay Hungry , Stay Foolish , Stay Patient , Stay Love !
  • 相关阅读:
    蓝牙遥控小车设计(二)——车体搭建和利用串口遥控小车
    WIN7下使用sublime text3替代arduino IDE(安装方法和所遇到的问题)
    在使用Arduino中遇到的问题(无法使用中文注释、程序无法下载)
    python 任务调度模块sched
    使用__all__限制模块可被导入对象
    python判断任务是CPU密集型还是IO密集型
    使用__slots__限制实例的属性
    使用装饰器获取被调用函数的执行的时间
    python上下文管理器
    http协议以及http1.0和http1.1的区别
  • 原文地址:https://www.cnblogs.com/henryyao/p/10374739.html
Copyright © 2011-2022 走看看