线性表数据结构

2019-05-28  本文已影响0人  YY_Lee
线性表

线性表就是数据排成像一条线的结构,每个线性表上的数据最多只有前和后两个方向。与线性表对立的是非线性表,如二叉树、堆、图就是非线性表结构。非线性表中,数据不只是简单的前后关系。线性表结构的数据结构有数组、链表、队列等。

数组

数组,用一组连续的内存空间存储一组相同类型的数据。

定义中牵扯到几个关键词:

数组的随机访问:

a[i]_address = base_address + i * data_type_size

base_address是数组首地址,data_type_size是每个元素的大小。因为相同元素,所以data_type_size相同。这也是为什么数组的小标通常从0开始,因为第一个元素的地址就是数组首地址。

高级语言中,针对数组类型都提供了容器类,如Java中的ArrayList、OC中的NSArray。这些容器类会将很多数组操作的细节封装起来,这些容器类都支持动态扩容,当存储空间不够时自动扩容为原来的若干倍。扩容涉及内存的申请和数据的搬移,比较耗时,如果事先知道存储的数据大小,最好在创建时事先指定好数组大小。

当遇到如下情况时优先选择使用数组:

对于业务开发,使用容器类就足够了,开发效率更高,损失的一点点性能不影响系统整体的性能。做非常底层的开发,如网络框架,性能优化要做到极致,优先使用数组。

链表

链表通过指针将一组零散的内存块串联在一起,每个内存块称为链表的结点。

这篇文章介绍三种常见的链表结构:单向链表、双向链表、循环链表。

单链表.jpg

单向链表只有一个方向,结点只有一个后继指针next指向后面的结点。单项链表有两个点比较特殊。第一个结点也叫头结点,是用来记录链表的基地址,有了它我们可以遍历整条链表。最后一个结点也叫尾结点,尾结点指向一个空地址Null,表示这是链表上最后一个结点。

链表也支持查找、插入、删除操作。跟数组相比,链表的插入和删除操作只需改变相邻结点的指针,对应的时间复杂度为O(1)。而链表因为内存不连续,先要访问第K个元素只能根据指针依次遍历结点,直到找到相应的结点,对应的时间复杂度为O(n)。

循环链表.jpg

循环链表的优点是从链尾到链头比较方便,当要处理的数据具有环形结构特点,适合采用循环列表,例如约瑟夫问题

双向链表.jpg

双向链表需要额外的空间存储后继和前驱结点的地址,所以双向链表占用内存更多,但可以支持双向遍历,使得双向链表在某些情况的下的插入、删除、查询操作更高效。比如删除指定指针指向的结点,因为双向链表中保存了前驱结点的指针,单向链表为了找到前驱结点要从头遍历。再比如向指定结点前插入一个结点,查询一个有序链表等,双向链表的效率都要更高。

这就是用“空间换时间”的设计思想。当内存空间充足,追求代码执行速度,可以选择空间复杂度相对高但时间复杂度低的算法或数据结构。反过来,内存紧缺,使用时间复杂度高但空间复杂度低的算法或数据结构,这就是“时间换空间”设计思想。

链表实现LRU缓存淘汰策略

缓存是一种提高数据读取性能的技术,但缓存大小有限,需要制定策略当缓存用满时,将哪些数据清理出去哪些数据保留。常见策略有三种:

下面说一些基于链表实现LRU缓存淘汰算法,维护一个有序单向链表,越靠近链表尾部的结点是越早访问的数据。当有新数据被访问,从链表头部开始遍历链表:

这样就用链表实现了LRU缓存。

栈是这样一种数据结构,越先存入栈中的数据,越后从栈中移除。后进者先出,先进者后出,这就是典型的栈结构。

当某个数据集合只涉及在一段插入和删除数据,且满足后进先出、先进后出的特性,应该首选栈这种数据结构。

栈的实现

栈可以用链表和数组实现,用数组实现的叫做顺序栈,用链表实现的叫做链式栈。

栈的应用

栈作为一个基础数据结构应用场景很多

表达式求值.jpg
队列

队列,就像排队买票,先来的先买后来的站在末尾排队。先进者先出,就是“队列”。队列也是一种操作受限的线性表数据结构。只能将数据放到队尾,叫做入队;从队列头部取元素,叫做出队;
队列可用数组和链表实现,数组实现的队列叫做顺序队列,链表实现的队列叫做链式队列。

队列.png

队列需要两个指针:head指针指向队头;tail指针,指向队尾。结合下图理解,a、b、c、d依次入队后,队列中的head指针指向0的位置,tail指针指向4的位置

dequeue_1.jpg

两次出队操作后,head指向2的位置,tail仍指向4的位置

dequeue_2.jpg

随着不停的入队出队,tail 和 head会持续向后移动。当tail移动到最右边时,即使数组中还有空间,也无法继续往队列中添加数据了。

此时需要通过数据搬移来解决这个问题,但如果每次出队都搬移整个队列的数据,出队的时间复杂度从O(1)变为O(n)。我们可以在没有空闲空间的情况下,入队时集中做一次数据搬移操作即可,这样出队保持不变。

循环队列

用数组来实现队列的时候,在 tail==n 时,会有数据搬移,这样入队操作性能就会受到影响,循环队列也可以解决这个问题。

CircularQueue.jpg

队列大小为8,当前head=4,tail=7。当再有新元素入队,放入7的位置,但不把tail更新为8,而是在环中后移一位到0的位置,再有新元素入队放入位置0。通过这样,避免了数据搬移。实现循环队列的关键是,确定好队满和队空的判定条件。

队满判断条件:head = tail;
队空判断条件:(tail+1)%n = head;

阻塞队列

阻塞队列是在队列基础上增加了阻塞条件。队列为空时,从队头取数据会被阻塞,直到队列中有了数据才能返回;队列已满,插入数据的操作会被阻塞,直到队列有空闲位置再插入。这个定义其实就是“生产者-消费者模型”,使用阻塞队列可以实现“生产者-消费者模型”。

并发队列

线程安全的队列叫并发队列。简单的实现是在出队和入队时加锁,但加锁粒度大并发度会降低,同一时刻只能运行一个存或取操作。

以上内容是对学习极客时间王争出品的数据结构与算法之美的总结,若感兴趣可以去订阅学习!

上一篇下一篇

猜你喜欢

热点阅读