2 基础数据结构
数组和链表是实现各种数据结构的基石,本节中的三种最基本的数据结构都可以用数组或者链表来实现。
2.1 栈
用数组实现“栈”非常简单。下面以C++为例,实现一个简单的固定大小的“栈”。
首先,接口API定义如下,核心函数就是push()和pop():
2.1.1 数组实现
下面用数组方式的实现“栈”,当然也可以用链表实现数组,但是一般尽管栈会处理很多操作,但任意时刻保存在栈中的元素不会很多,所以使用数组实现比链表会更加高效:
Loitering问题
要特别注意的是:当我们实现数据结构,自己管理一块内存区域(如一直持有数组或链表做成员变量)时,一定要注意内存泄漏,或称Loitering问题。如上面的栈实现,push()和pop()只是移动变量N,但当数组中保存的是对象时,pop()就会造成数组依然持有无用对象的引用或指针。因此,《算法》中的pop()实现考虑到了这一点,并加入了resize()功能。
动态调整大小
上面的代码同时也实现了resize()功能,当栈的大小等于总容量(push时检测),或者栈的大小等于总容量的1/4时,栈底层的数组就会扩张或者收缩一倍,这样总能保持数组有一半的空闲空间。方法很简单,就是新建一个数组,将旧元素拷贝过去。
当然,实际上JDK的ArrayList设计要比这复杂,JDK 6的实现是oldCapacity*3/2+1。值得注意的是remove()时并不会收缩数组。JDK的Stack是基于Vector,实现类似,但需要在构造方法中提供一个参数capacityIncrement显式指定增长多少。
2.2.2 链表实现
值得注意的是:用链表实现时,因为不会一直持有一块内存空间,所以既不需要动态调整大小避免空间不足或浪费,也不用考虑loitering问题。直接操作目标结点,删除时释放结点的空间(C++)或直接交给GC(Java)。
2.2.3 迭代
除了数据结构提供的基本操作函数,有时我们经常要逐个遍历处理所有元素。这时,迭代器就是一种非常强大的工具,它可以将客户端从数据结构底层的具体实现抽离出来。《STL源码解析》一语道破天机:STL的中心思想在于,将算法与数据结构分离,彼此独立设计,最后用迭代器这个粘合剂将它们撮合到一起。不仅是Java(对迭代器的支持就很好,JDK本身就提供了Iterable和Iterator接口,实现后可以自动集成foreach进行遍历),在C++中迭代器在STL中的地位更高:
2.2 队列
队列用链表实现非常简单,这里介绍一下如何用数组实现。因为“栈”是从一端进出的,所以N到达数组尾部时就意味着“栈”满了需要resize。然而队列是从两堆进出的,即使到达数组尾部了,数组头部仍然可能有出队产生的空闲空间,这样我们可以用“回绕”解决这个问题。
2.2.1 数组实现
除了使用first和last两个下标标记外,一定要保存当前大小,否则仅仅从first和last的位置是很难判断队列是空的还是满的。
2.2.2 Ring Buffer
在《LMAX高并发系统架构》中,高并发框架Disruptors使用Ring Buffer保存缓冲的数据。Ring Buffer与数组实现的队列的区别是:没有尾指针,从不会出队,入队操作会覆盖已有数据。因为入队会覆盖元素,所以要靠生产者和消费者自己控制速度,避免未处理的元素被覆盖。