链表
像数组一样,链表也用来表示一系列的元素。事实上,能用数组来做的事情,一般也可以用 链表来做。然而,链表的实现跟数组是不一样的,在不同场景它们会有不同的性能表现。
与数组不同的是,组成链表的格子不是连续的。它们可以分布在内存的各个地方。这种不相邻的格子,就叫作结点
每个结点除了保存数据,它还保存着链表里的下一结点的内存地址
这份用来指示下一结点的内存地址的额外数据,被称为链。链表如下图所示:
此例中,我们的链表包含 4 项数据:"a"、"b"、"c"和"d"。因为每个结点都需要 2 个格子, 头一格用作数据存储,后一格用作指向下一结点的链(最后一个结点的链是 null,因为它是终 点),所以整体占用了 8 个格子。
若想使用链表,你只需知道第一个结点在内存的什么位置。因为每个结点都有指向下一结点 的链,所以只要有给定的第一个结点,就可以用结点 1 的链找到结点 2,再用结点 2 的链找到结 点 3……如此遍历链表的剩余部分。
链表相对于数组的一个好处就是,它可以将数据分散到内存各处,无须事先寻找连续的空格子。
实现一个链表
class Node
attr_accessor :data, :next_node
def initialize(data)
@data = data
end
end
/*Node 类有两个属性:data 表示结点所保存的数据,next_node 表示指向下一结点的链*/
node_1 = Node.new("once")
node_2 = Node.new("upon")
node_1.next_node = node_2
node_3 = Node.new("a")
node_2.next_node = node_3
node_4 = Node.new("time")
node_3.next_node = node_4
//以上代码创建了 4个连起来的结点,它们分别保存着"once"、"upon"、"a"和"time" 4项数据。
虽然只用 Node 也可以创建出链表,但我们的程序无法由此轻易地得知哪个结点是链表的开 端。因此我们还得创建一个 LinkedList 类。下面是一个最基本的 LinkedList 的写法。
class LinkedList
attr_accessor :first_node
def initialize(first_node)
@first_node = first_node
end
end
有了这个类,我们就可以用以下代码让程序知道链表的起始位置了。
list = LinkedList.new(node_1)
LinkedList 的作用就是一个指针,它指向链表的第一个结点。
假设程序要读取链表中索引 2 的值,计算机不可能在一步之内完成,因为无法一下子算出它 在内存的哪个位置。毕竟,链表的结点可以分布在内存的任何地方。程序知道的只有第 1 个结点 的内存地址,要找到索引 2 的结点(即第 3 个),程序必须先读取索引 0 的链,然后顺着该链去 找索引 1。接着再读取索引 1 的链,去找索引 2,这才能读取到索引 2 里的值。
下面我们在 LinkedList 类中加入读取操作。
class LinkedList
attr_accessor :first_node
def initialize(first_node)
@first_node = first_node
end
def read(index)
# 从第一个结点开始
current_node = first_node
current_index = 0
while current_index < index do
# 顺着链往下找,直至我们要找的那个索引值
current_node = current_node.next_node
current_index += 1
# 如果读到最后一个结点之后,就说明
# 所找的索引不在链表中,因此返回 nil
return nil unless current_node
end
return current_node.data
end
end
#当想要读取某个索引时,可以这样写:
list.read(3)
查找
class LinkedList
attr_accessor :first_node
def index_of(value)
# 从第一个结点开始
current_node = first_node
current_index = 0
begin
# 如果找到,就返回
if current_node.data == value
return current_index
end
# 否则,看下一个结点
current_node = current_node.next_node
current_index += 1
end while current_node
# 如果遍历整个链表都没找到,就返回 nil
return nil
end
end
#有了它我们就可以这样来查找了:
list.index_of("time")
插入
下面给 LinkedList 类加上插入方法。
class LinkedList
attr_accessor :first_node
def insert_at_index(index, value)
# 创建新结点
new_node = Node.new(value)
# 如果在开头插入,则将新结点的 next_node 指向原 first_node,
# 并为其设置新的 first_node
if index == 0
new_node.next_node = first_node
return @first_node = new_node
end
current_node = first_node
current_index = 0
# 先找出新结点插入位置前的那一结点
prev_index = index - 1
while current_index < prev_index do
current_node = current_node.next_node
current_index += 1
end
new_node.next_node = current_node.next_node
# 使前一结点的链指向新结点
current_node.next_node = new_node
end
end
删除
LinkedList 类的删除操作实现如下。
class LinkedList
attr_accessor :first_node
def delete_at_index(index)
# 如果删除的是第一个结点,
# 则将 first_node 重置为第二个结点,
# 并返回原第一个结点
if index == 0
deleted_node = first_node
@first_node = first_node.next_node
return deleted_node
end
current_node = first_node
current_index = 0
# 先找出被删结点前的那一结点,
# 将其命名为 current_node
while current_index < index - 1 do
current_node = current_node.next_node
current_index += 1
end
# 再找出被删结点后的那一结点
deleted_node = current_node.next_node
node_after_deleted_node = deleted_node.next_node
# 将 current_node 的链指向 node_after_deleted_node,
# 这样被删结点就被排除在链表之外了
current_node.next_node = node_after_deleted_node
deleted_node
end
end
链表与数组的性能对比如下所示:
双向链表
用代码来表述的话,如下所示。
class Node
attr_accessor :data, :next_node, :previous_node
def initialize(data)
@data = data
end
end
class DoublyLinkedList
attr_accessor :first_node, :last_node
def initialize(first_node=nil, last_node=nil)
@first_node = first_node
@last_node = last_node
end
end
插入
这里创建了一个新结点("Sue"),并使其 previous_node 指向双向链表的 last_node ("Greg")。然后,再将 last_node("Greg")的 next_node 指向这个新结点("Sue")。最后, 把 last_node 改为新结点("Sue")。
以下是在双向链表中实现的新方法 insert_at_end。
class DoublyLinkedList
attr_accessor :first_node, :last_node
def initialize(first_node=nil, last_node=nil)
@first_node = first_node
@last_node = last_node
end
def insert_at_end(value)
new_node = Node.new(value)
# 如果链表还没有任何结点
if !first_node
@first_node = new_node
@last_node = new_node
else
new_node.previous_node = @last_node
@last_node.next_node = new_node
@last_node = new_node
end
end
end
因为双向链表能直接访问前端和末端的结点,所以在两端插入的效率都为 O(1),在两端删除的效率也为 O(1)。由于在末尾插入和在开头删除都能在 O(1)的时间内完成,因此拿双向链表作为队列的底层数据结构就最好不过了。
以下是基于双向链表的队列的完整代码示例。
class Node
attr_accessor :data, :next_node, :previous_node
def initialize(data)
@data = data
end
end
class DoublyLinkedList
attr_accessor :first_node, :last_node
def initialize(first_node=nil, last_node=nil)
@first_node = first_node
@last_node = last_node
end
def insert_at_end(value)
new_node = Node.new(value)
# 如果链表还没有任何结点
if !first_node
@first_node = new_node
@last_node = new_node
else
new_node.previous_node = @last_node
@last_node.next_node = new_node
@last_node = new_node
end
end
def remove_from_front
removed_node = @first_node
@first_node = @first_node.next_node
return removed_node
end
end
class Queue
attr_accessor :queue
def initialize
@queue = DoublyLinkedList.new
end
def enque(value)
@queue.insert_at_end(value)
end
def deque
removed_node = @queue.remove_from_front
return removed_node.data
end
def tail
return @queue.last_node.data
end
end
栈
栈存储数据的方式跟数组一样,都是将元素排成一行。只不过它还有以下 3 条约束。
只能在末尾插入数据。
只能读取末尾的数据。
只能移除末尾的数据。
你可以将栈看成一叠碟子:你只能看到最顶端那只碟子的碟面,其他都看不到。另外,要加 碟子只能往上加,不能往中间塞,要拿碟子只能从上面拿,不能从中间拿(至少你不应该这么做)。 绝大部分计算机科学家都把栈的末尾称为栈顶,把栈的开头称为栈底。
队列
队列对于临时数据的处理也十分有趣,它跟栈一 样都是有约束条件的数组。区别在于我们想 要按什么顺序去处理数据,而这个顺序当然是要取决于具体的应用场景。
你可以将队列想象成是电影院排队。
排在最前面的人会最先离队进入影院。套用到队列上, 就是首先加入队列的,将会首先从队列移出。因此计算机科学家都用缩写“FIFO”(first in, first out) 先进先出,来形容它。
与栈类似,队列也有 3 个限制(但内容不同)。
只能在末尾插入数据(这跟栈一样)。
只能读取开头的数据(这跟栈相反)。
只能移除开头的数据(这也跟栈相反)。