表、栈和队列
这是最基本的三种数据结构,每个程序中都至少使用一种,比如说栈,递归程序其实就是借助方法调用栈来实现的,站在计算机的角度,没有什么问题是栈解决不了的。本文对这些数据结构进行说明。随后会在这个 GitHub 整理出相应的练习题和对应标签的 LeetCode 题目解答。
本文将讨论:
- 表、栈和队列相关概念,特点和基本操作
1. 表
表是一种逻辑结构,表示元素之间的相邻关系。按照元素在内存中的不同表现形式可分为:顺序存储和链式存储。
1.1 顺序存储
顾名思义,先申请一块大小固定并且连续的内存空间,然后把元素放进去。一般情况下,我们事先不知道需要多大的空间,过大会浪费,过小还得扩充,局限性比较大。顺序表基于数组实现,随机访问的时间复杂度为O(1);遍历和查找的时间复杂度为O(n);插入,删除,最坏的情况下(O(n)),因为需要移动大量元素。
1.2 链式存储(单链表,双链表,循环链表)
为了避免插入,删除影响运行效率,由此引入链式存储,它不需要连续的存储空间,通过“链”建立元素之间的关系,在插入、删除时,只需要修改指针,不用移动元素。经常使用的是单链表,对于链表节点,除了存储自身数据外,还存放一个指向后继的指针,在Java中就是保留后继元素的一个引用。单链表中查找某个特定的元素,只能从头开始遍历。在表示单链表时,通常会增加一个“头结点”,这样的好处是,在处理第一个节点时和其他节点没有差别,单链表的结构如下:
图 1-1 单链表
1.2.1 链表常用操作
链表又分为单链表,双链表和循环链表,链表常用的操作就是插入和删除,只需要修改指针即可,具体如下。
(1)单链表结点的插入和删除,其实在插入之前需要找到,此位置的前驱结点然后执行插入,这里我们假设前驱结点是已知的。
(2)双链表结点的插入和删除,与单链表的区别是,它得修改前驱和后继两个指针
(3)循环链表,不管是单链表还是双链表只需要把尾结点指向头结点即可形成,如带头结点的单链表头尾相连:
2. 栈
栈,在计算机中运用广泛,比如说JVM,它就是基于栈来执行指令的。栈最大的特点就是先进后出(Last In First Out,LIFO),所以入栈序列和出栈序列是相反的。栈另一个特点是只允许在栈顶top操作,俩动作入栈 push 和出栈 pop。栈的结构图如下:
3. 队列
队列,在现实生活中,买票和要排队,进地铁要排队,上地铁也要排队……,无时无刻都在体现队列的应用。由于售票厅和地铁它们的处理能力存在瓶颈,为了防止大规模的事件请求,采用排队机制。在计算机中也是这个道理,比如打印机,主机请求的次数,比打印机要快的多,为了匹配速度,可以建立一个缓冲区,满了主机就不要请求了,打印机就不断从缓冲区拿数据打印就行。
队列的特点是,先进先出(First In First Out,FIFO),只允许在队尾 tail 插入,在队首 head 删除,其结构如下:
队列有一个很重要的实现,叫环形队列,经常被用来实现环形缓冲区,它的指针移动特点是使用模运算。比较常用的判断环形队列是否为空方法是:牺牲一个存储单元,约定”队头指针在队尾指针的下一个位置作为队满的标志”,本文给出一个 RingBuffer 的简单实现,当队列满时,丢弃新来的数据,并记录。