Java 中的数据结构和算法,第 5 部分:双向链表

使用双向链表和循环链表及其算法进行高级搜索和排序

虽然单向链表有很多用途,但它们也存在一些限制。一方面,单向链表将节点遍历限制在一个方向:除非您首先反转其节点链接,否则您无法向后遍历单向链表,这需要时间。如果你做反向遍历,需要将节点遍历恢复到原来的方向,你将不得不重复反演,这需要更多的时间。单链表也限制节点删除。在这种类型的列表中,您不能在无法访问节点的前任的情况下删除任意节点。

幸运的是,Java 提供了多种类型的列表,您可以使用它们来搜索和排序 Java 程序中存储的数据。数据结构和算法系列的最后一个教程介绍了使用双向链表和循环链表进行搜索和排序。正如您将看到的,这两个数据结构类别建立在单链表上,以在您的 Java 程序中提供更广泛的搜索和排序行为。

双向链表

甲双向链表是其中每个节点具有一对链接字段的节点的链接列表。一个链接字段允许您向前遍历列表,而另一个节点允许您向后遍历列表。对于前向,引用变量保存对第一个节点的引用。每个节点通过“下一个”链接字段链接到下一个节点,除了最后一个节点,其“下一个”链接字段包含空引用以表示列表的结尾(在前向方向)。向后方向的工作方式类似。引用变量保存对前向最后一个节点的引用,您将其解释为第一个节点。每个节点通过“上一个”链接字段链接到前一个节点。第一个节点的“上一个”

尝试将双向链表视为一对单向链表,每个链表互连相同的节点。图 1 中的图表显示了topForward-referenced 和topBackward-referenced 单链表。

双向链表中的 CRUD 操作

创建、插入、删除节点都是双向链表中常见的操作。它们类似于您为单链表学习的操作。(请记住,双向链表只是将相同节点互连的一对单链表。)以下伪代码演示了如何创建节点并将节点插入到图 1 所示的双向链表中。伪代码还演示了节点删除:

图 1. 向前和向后遍历双向链表


DECLARE CLASS Node
   DECLARE STRING name
   DECLARE Node next
   DECLARE Node prev
END DECLARE
DECLARE Node topForward
DECLARE Node temp
DECLARE Node topBackward
topForward = NEW Node
topForward.name = "A"
temp = NEW Node
temp.name = "B"
topBackward = NEW Node
topBackward.name = "C"
// Create forward singly-linked list
topForward.next = temp
temp.next = topBackward
topBackward.next = NULL
// Create backward singly-linked list
topBackward.prev = temp
temp.prev = topForward
topForward.prev = NULL
// Delete Node B.
temp.prev.next = temp.next; // Bypass Node B in the forward singly-linked list.
temp.next.prev = temp.prev; // Bypass Node B in the backward singly-linked list.
END

示例应用:双向链表中的 CRUD

示例 Java 应用程序DLLDemo演示了如何在双向链表中创建、插入和删除节点。该应用程序的源代码如清单 1 所示。

清单 1. 一个在双向链表中演示 CRUD 的 Java 应用程序


public final class DLLDemo
{
   private static class Node
   {
      String name;
      Node next;
      Node prev;
   }

   public static void main(String[] args)
   {
      // Build a doubly-linked list.

      Node topForward = new Node();
      topForward.name = "A";
      Node temp = new Node();
      temp.name = "B";
      Node topBackward = new Node();
      topBackward.name = "C";
      topForward.next = temp;
      temp.next = topBackward;
      topBackward.next = null;
      topBackward.prev = temp;
      temp.prev = topForward;
      topForward.prev = null;

      // Dump forward singly-linked list.

      System.out.print("Forward singly-linked list: ");
      temp = topForward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.next;
      }
      System.out.println();

      // Dump backward singly-linked list.

      System.out.print("Backward singly-linked list: ");
      temp = topBackward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.prev;
      }
      System.out.println();

      // Reference node B.

      temp = topForward.next;

      // Delete node B.

      temp.prev.next = temp.next;
      temp.next.prev = temp.prev;

      // Dump forward singly-linked list.

      System.out.print("Forward singly-linked list (after deletion): ");
      temp = topForward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.next;
      }
      System.out.println();

      // Dump backward singly-linked list.

      System.out.print("Backward singly-linked list (after deletion): ");
      temp = topBackward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.prev;
      }
      System.out.println();
   }
}

编译清单 4 如下:

javac DLLDemo.java

运行生成的应用程序,如下所示:

java DLLDemo

您应该观察到以下输出:

Forward singly-linked list: ABC
Backward singly-linked list: CBA
Forward singly-linked list (after deletion): AC
Backward singly-linked list (after deletion): CA

在双向链表中混洗

ava Collections Framework 包括一Collections类实用程序方法,它是java.util包的一部分。此类包括一个void shuffle(List<?> list)“使用默认随机源随机排列指定列表”的方法。例如,您可以使用此方法对表示为双向链表的一副牌进行洗牌(java.util.LinkedList该类就是一个示例)。在下面的伪代码中,您可以看到Shuffle 算法如何对双向链表进行打乱:


DECLARE RANDOM rnd = new RANDOM
DECLARE INTEGER i
FOR i = 3 DOWNTO 2
   swap(topForward, i - 1, rnd.nextInt(i))
END FOR
FUNCTION swap(Node top, int i, int j)
   DECLARE Node nodei, nodej
   DECLARE INTEGER k
   // Locate ith node.
   Node nodei = top
   FOR k = 0 TO i - 1
      nodei = nodei.next
   END FOR
   // Locate jth node.
   Node nodej = top
   FOR k = 0 TO i - 1
      nodej = nodej.next
   END FOR
   // Perform the swap.
   DECLARE STRING namei = nodei.name
   DECLARE STRING namej = nodej.name
   nodej.name = namei
   nodei.name = namej
END FUNCTION
END

Shuffle 算法获取一个随机源,然后向后遍历列表,从最后一个节点一直到第二个。它反复将随机选择的节点(实际上只是名称字段)交换到“当前位置”。节点是从列表中从第一个节点到当前位置(含)的部分中随机选择的。请注意,该算法大致摘自void shuffle(List<?> list)的源代码。

Shuffle 算法伪代码是惰性的,因为它只关注向前遍历的单向链表。这是一个合理的设计决策,但我们为此付出了时间复杂度的代价。时间复杂度为 O( n 2 )。首先,我们有 O( n ) 循环调用swap(). 其次,在 内swap(),我们有两个连续的 O( n ) 循环。回忆一下第 1 部分中的以下规则:


If f1(n) = O(g(n)) and f2(n) = O(h(n)) then

(a) f1(n)+f2(n) = max(O(g(n)), O(h(n)))
(b) f1(n)*f2(n) = O(g(n)*h(n)).

(a) 部分处理顺序算法。在这里,我们有两个 O( n ) 循环。根据规则,由此产生的时间复杂度将是 O( n )。(b) 部分处理嵌套算法。在这种情况下,我们将 O( n ) 乘以 O( n ),结果为 O( n 2 )。

请注意,Shuffle 的空间复杂度为 O(1),这是由声明的辅助变量产生的。

示例应用:在双向链表中混洗

Shuffle清单 2 中的应用程序演示了 Shuffle 算法。

清单 2. Java 中的 Shuffle 算法


import java.util.Random;

public final class Shuffle
{
   private static class Node
   {
      String name;
      Node next;
      Node prev;
   }

   public static void main(String[] args)
   {
      // Build a doubly-linked list.

      Node topForward = new Node();
      topForward.name = "A";
      Node temp = new Node();
      temp.name = "B";
      Node topBackward = new Node();
      topBackward.name = "C";
      topForward.next = temp;
      temp.next = topBackward;
      topBackward.next = null;
      topBackward.prev = temp;
      temp.prev = topForward;
      topForward.prev = null;

      // Dump forward singly-linked list.

      System.out.print("Forward singly-linked list: ");
      temp = topForward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.next;
      }
      System.out.println();

      // Dump backward singly-linked list.

      System.out.print("Backward singly-linked list: ");
      temp = topBackward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.prev;
      }
      System.out.println();

      // Shuffle list.

      Random rnd = new Random();
      for (int i = 3; i > 1; i--)
         swap(topForward, i - 1, rnd.nextInt(i));

      // Dump forward singly-linked list.

      System.out.print("Forward singly-linked list: ");
      temp = topForward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.next;
      }
      System.out.println();

      // Dump backward singly-linked list.

      System.out.print("Backward singly-linked list: ");
      temp = topBackward;
      while (temp != null)
      {
         System.out.print(temp.name);
         temp = temp.prev;
      }
      System.out.println();
   }

   public static void swap(Node top, int i, int j)
   {
      // Locate ith node.

      Node nodei = top;
      for (int k = 0; k < i; k++)
         nodei = nodei.next;

      // Locate jth node.

      Node nodej = top;
      for (int k = 0; k < j; k++)
         nodej = nodej.next;

      String namei = nodei.name;
      String namej = nodej.name;
      nodej.name = namei;
      nodei.name = namej;
   }
}

编译清单 5 如下:


javac Shuffle.java

运行生成的应用程序,如下所示:

java Shuffle

您应该从一次运行中观察到以下输出:

Forward singly-linked list: ABC
Backward singly-linked list: CBA
Forward singly-linked list: BAC
Backward singly-linked list: CAB

循环链表

单向链表的最后一个节点中的链接字段包含一个空链接。在双向链表中也是如此,它包含前向和反向单向链表的最后节点中的链接字段。相反,假设最后一个节点包含到第一个节点的链接。在这种情况下,您最终会得到一个循环链表,如图 2 所示。

图 2. 一个循环链表将最后一个节点连接到第一个节点

循环链表,也称为循环缓冲区或循环队列,有很多用途。例如,操作系统中断处理程序使用它们来缓冲击键。多媒体应用程序使用循环链表来缓冲数据(例如,缓冲写入声卡的数据)。LZ77系列无损数据压缩算法也使用此技术。

链表与数组

在这个关于数据结构和算法的系列文章中,我们已经考虑了不同数据结构的优点和缺点。由于我们专注于数组和链表,您可能对这些类型有特别的疑问。链表和数组有哪些优缺点?什么时候用链表,什么时候用数组?两个类别的数据结构是否可以集成到有用的混合数据结构中?我将在下面尝试回答这些问题。

与数组相比,链表具有以下优点:

  • 它们不需要额外的内存来支持扩展。相反,当需要扩展时,数组需要额外的内存。(一旦所有元素都包含数据项,就不能将新的数据项附加到数组中。)
  • 与等效的基于数组的操作相比,它们提供更快的节点插入/删除。只有在确定插入/删除位置后才需要更新链接。从数组的角度来看,数据项插入需要移动所有其他数据项以创建一个空元素。类似地,删除现有数据项需要移动所有其他数据项以移除空元素。所有数据项移动都需要时间。

相比之下,数组与链表相比具有以下优势:

  • 数组元素比节点占用更少的内存,因为元素不需要链接字段。
  • 数组通过基于整数的索引提供对数据项的更快访问。

总之,链表最适合处理动态数据,即插入和删除频繁的程序。相比之下,数组最适合数据是静态的程序,这意味着很少插入或删除新数据项。(回想一下,如果在向数组中添加数据项时空间不足,则必须创建一个更大的数组,将原始数组的数据项复制到更大的数组中,然后处理原始数组。这需要时间,这会影响性能——尤其是重复执行时。)

您可能认为将单向链表与一维数组合并以通过数组索引访问节点将无济于事。你会浪费内存,因为你需要数组元素和节点,你会浪费时间,因为你需要在插入或删除节点时移动数组的数据项。事实上,将数组与链表集成以创建混合是有益的!虽然它超出了本系列的范围,但哈希表是数组/链表合作的一个很好的例子。

教程系列结语

本教程系列介绍了数据结构和算法的基础知识。我专注于两个数据结构类别,Java 数组和链表,它们是更复杂的数据结构(如堆栈、队列、树、图、字典/映射和集合)的基础。我鼓励你继续探索和学习数据结构和算法。作为起点,Java Collections Framework包含许多您可以探索的有用数据结构和算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值