1.单链表
在 Java 中没有显式的指针类型,然而实际上对象的访问就是使用指针来实现的,即在Java 中是使用对象的引用来替代指针的。因此在使用 Java 实现该结点结构时,一个结点本身就是一个对象。结点的数据域 data 可以使用一个 Object 类型的对象来实现,用于存储任何类型的数据元素,并通过对象的引用指向该元素;而指针域 next 可以通过节点对象的引
用来实现。
单链表结点结构是结点的一种最简单的形式,除此之外还有其他不同的结点结构,但是这些结点结构都有一个数据域,并均能完成数据元素的存取。为此在使用 Java 定义单链表结点结构之前先给出一个结点接口,在接口中定义了所有结点均支持的操作,即对结点中存储数据的存取。
结点接口
public interface Node { //获取结点数据域 public Object gerData(); //设置结点数据域 public void setData(Object obj); } 单链表结点定义 public class SLNode implements Node { public Object element; public SLNode next; public SLNode() { this(null, null); } public SLNode(Object ele, SLNode next) { this.element = ele; this.next = next; } public SLNode getNext() { return next; } public void setNext(SLNode next) { this.next = next; } /** ************** Methods of Node Interface ************* */ public Object gerData() { return element; } public void setData(Object obj) { element = obj; } }
单链表结构
链表的第一个结点和最后一个结点,分别称为链表的首结点和尾结点。尾结点的特征是其 next 引用为空( null)。链表中每个结点的 next 引用都相当于一个指针,指向另一个结点,借助这些 next 引用,我们可以从链表的首结点移动到尾结点。在单链表中,经常用head引用来指向链表的首结点,由head引用可以完成对链表所有结点的访问,当然有时候也会用指向尾结点的tail引用来方便完成某些操作的实现。
需要注意的是:在单链表结构中还需要注意的一点是,由于每个结点的数据域都是一个
Object 类的对象。
与数组类似,单链表中的结点也具有一个线性次序,即如果结点 P 的 next 引用指向结点 S,则 P 就是 S 的直接前驱, S 是 P
的直接后续。单链表的一个重要特性就是只能通过前驱结点找到后续结点,而无法从后续结点找到前驱结点。在单链表中通常需要完成数据元素的查找、插入、删除等操作。
(1)查找:在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的 next 引用来一次访问链表中的每个结点以完成相应的查找操作。
在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的 next 引用来一次访问链表中的每个结点以完成相应的查找操作。
除了单链表的首结点由于没有直接前驱结点,所以可以直接在首结点之前插入一个新的结点之外,在单链表中的其他任何位置插入一个新结点时,都只能是在已知某个特定结点引用的基础上在其后面插入一个新结点。并且在已知单链表中某个结点
引用的基础上,完成结点的插入操作需要的时间是Θ(1)。由于在单链表中数据元素的插入是通过节点的插入来完成的,因此在单链表中完成数据元素的插入操作要比在数组中完成数据元素的插入操作所需Ο(n)的时间要快得多。
(3)删除
链表的删除和插入相似,数据元素的删除也是通过对结点的操作完成的。在不同的位置,操作会略有不同。
在单链表中删除一个结点时,除首结点外都必须知道该结点的直接前驱结点的引用。并且在已知单链表中某个结点引用的基础上,完成其后续结点的删除操作需要的时间是Θ
(1)。由于在单链表中数据元素的删除是通过节点的删除来完成的,因此
在单链表中完成数据元素的删除操作要比在数组中完成数据元素的删除操作所需Ο (n)的时间要快得多。
(4)结论:在单链表中进行顺序查找与在数组中完成相同操作具有相同的时间复杂度,而在单链表中在已知特定结点引用的前提下完成数据元素的插入与删除操作要比在数组中完成相同操作快得多。
<strong>2.单链表的实现 class LinkList { private Node head = null; private Node tail = null; int Length = 0; public Node getHead() { return head; } public void setHead(Node head) { this.head = head; } public Node getTail() { return tail; } public void setTail(Node tail) { this.tail = tail; } // 判断链表是否为空 public boolean isEmpty() { return (Length == 0); } // 查找指定位置的结点 public Node FindKth(int k) { Node temp = head; int i = 1; while (temp != null && i < k) { // 移动元素,直到找到相应位置 temp = temp.next; i++; } if (i == k) return temp; else return null; } // 查找指定元素所在的位置 public int Find(String str) { Node temp = head; int current = 1; while (temp != null && temp.key.equals(str)) { temp = temp.next; current++; } if (temp.key.equals(str)) return current; else return -1; } // 头插法 public void insertHead(String str) { Node newLink = new Node(str); newLink.next = head; head = newLink; Length++; } // 将结点插入指定位置 public void insert(int index, String str) { Node newLink = new Node(str); if (index > 1 && index < Length) { newLink.next = FindKth(index - 1).next; FindKth(index - 1).next = newLink; // System.out.println(head.key); 此时head的值为4 Length++; } else if (index == 1) { newLink.next = head; head = newLink; Length++; } else { System.out.println("超出链表长度,插入无效"); } } // 删除指定位置的结点 public void delete(int index) { if (index == 1) { // 删除位置为头结点 head = head.next; Length--; } else if (index > 1 && index < Length) { // 删除位置不为头结点或尾结点 Node temp = FindKth(index).next; FindKth(index - 1).next = temp; Length--; } else { System.out.println("超出链表长度,查找无效"); } } // 删除指定元素 public void deleteNode(String str) { int current = Find(str); if (current > 1 && current < Length) { // 删除的位置为头结点 Node temp = FindKth(current).next; FindKth(current - 1).next = temp; Length--; } else if (current == 1) { head = head.next; Length--; } else if (current == -1) { System.out.println("要删除的元素不存在,请重新选择!"); } } // 初始化链表 public void initList(Node node) { head = node; head.next = tail; } // 打印单链表 public void display() { Node current = head; while (current != null) { current.displayNode(); current = current.next; } } } public class NodeTest { public static void main(String args[]) { LinkList list = new LinkList(); list.insertHead("1"); list.insertHead("2"); list.insertHead("3"); list.insertHead("4"); System.out.println("头插法操作后的单链表:"); list.display(); System.out.println(); // 把元素插入指定位置 int index = 1; System.out.println("插入元素后的单链表:"); list.insert(index, "a"); list.display(); // 删除元素 System.out.println(); System.out.println("删除元素后的单链表(删除指定元素):"); list.deleteNode("7"); list.display(); // 删除元素 System.out.println(); System.out.println("删除元素后的单链表(删除结点位置):"); list.delete(4); list.display(); } } class Node { public String key; public Node next;// 指向下一个元素的指针 // 初始化头结点 public Node(String str) { this.key = str; this.next = null; } public void displayNode() { System.out.print(key + " -> "); } }</strong>
3.基于时间的比较
线性表有查找,删除,插入三类操作。
对于查找操作有基于序号的查找,即存取线性表中 i 号数据元素。由于数组有随机存取的特性,在线性表的顺序存储实现中可以在Θ(1)的时间内完成;而在链式存储中由于需要从头结点开始顺着链表才能取得,无法在常数时间内完成,因此顺序存储优于链式存储。查找操作还有基于元素的查找,即线性表是否包含某个元素、元素的序号是多少,这类操作线性表的顺序存储与链式存储都需要从线性表中序号为 0 的元素开始依次查找,因此两种实现的性能相同。综上所述,如果在线性表的使用中主要操作是查找,那么应当选用顺序存储实现的线性表。
对于基于数据元素的插入、删除操作而言,当使用数组实现相应操作时,首先需要采用顺序查找定位相应数据元素,然后才能插入、删除,并且在插入、删除过程又要移动大量元素;相对而言链表的实现只需要在定位数据元素的基础上,简单的修改几个指针即可完成,因此链式存储优于顺序存储。对于基于序号的插入、删除操作,因为在顺序存储中平均需要移动一半元素;而在链式存储中不能直接定位,平均需要比较一半元素才能定位。因此顺序存储与链式存储性能相当。综上所述,如果在线性表的使用中主要操作是插入、删除操作,那么选用链式存储的线性表为佳。
4.基于空间的比较
线性表的顺序存储,其存储空间是预先静态分配的,虽然在实现的过程中可以动态扩展数组空间,但是如果线性表的长度变化范围较大,空间在使用过程中由于会存在大量空闲空间,使得存储空间的利用率不高。而线性表的链式存储,其结点空间是动态分配的,不会存在存储空间没有完全利用的情况。因此当线性表长度变化较大时,宜采用链式存储结构。当线性表的数据元素结构简单,并且线性表的长度变化不大时。由于链式存储结构使用了额外的存储空间来表示数据元素之间的逻辑关系,因此针对数据域而言,指针域所占比重较大;而在线性表的顺序存储结构中,没有使用额外的存储空间来表示数据元素之间的逻辑关系,尽管有一定的空闲空间没有利用,但总体而言由于线性表长度变化不大,因此没有利用的空间所占比例较小。所以当线性表数据元素结构简单,长度变化不大时可以考虑采用顺序存储结构。