基于结点的数据结构——链表

链表

像数组一样,链表也用来表示一系列的元素。事实上,能用数组来做的事情,一般也可以用 链表来做。然而,链表的实现跟数组是不一样的,在不同场景它们会有不同的性能表现。

与数组不同的是,组成链表的格子不是连续的。它们可以分布在内存的各个地方。这种不相邻的格子,就叫作结点

每个结点除了保存数据,它还保存着链表里的下一结点的内存地址

这份用来指示下一结点的内存地址的额外数据,被称为链。链表如下图所示:

此例中,我们的链表包含 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 个限制(但内容不同)。

 只能在末尾插入数据(这跟栈一样)。

 只能读取开头的数据(这跟栈相反)。

 只能移除开头的数据(这也跟栈相反)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值