下面,笔者和大家来一起简单回忆一下单链表,链表用一组任意的存储单元存放元素,存储单元可以连续也可不连续。
在这里笔者定义了一个节点类: Node
nodeValue | next |
/**
* 单链表时Node 存储;两个变量: nodeValue next 这里没有设置Node 的属性为 private
* 主要是为了其他类访问方便,若要进行保护,可以提供相关的get set方法进行访问
*
*/
public class Node<T> {
T nodeValue; // 数据域
Node<T> next; // 指针域保存着下一节点的引用
public Node(T nodeValue, Node<T> next) {
this.nodeValue = nodeValue;
this.next = next;
}
public Node(T nodeValue) {
this(nodeValue, null);
}
}
接下来笔者定义了一个
单链表 SingleLinkedList 这是一个带头结点的单链表。每个单链表有一个头指针 head
// 下面是SingleLinkedList类的数据成员和方法 head 指向表头
private Node<T> head;
public SingleLinkedList() {
head = null;
}
插入: 由于单链表只有一个头指针,所以插入分一下3中情况讨论。
1、表头插入时,由于有表头指针,可直接进行插入,效率非常高时间复杂度为常数
2、表尾插入时,由于只有一个表头指针没有表尾指针,在表尾插入时需要遍历整个单链表知道找到next引用为空时才进行插入。时间复杂度为O(n).
3、指定节点插入时, 同样需要遍历链表找到插入位置,时间复杂度同样为O(n)。
/**
* 在表头插入结点,效率非常高,由于有对表头的引用
*/
public void insertFirst(T t) {
Node<T> newNode = new Node<T>(t);
if (isEmpty()) {
head = newNode; // 链表为空为空,将head指向 newNode
} else {
newNode.next = head;
head = newNode;
}
}
/**
* 这个是没有尾指针时,在链表尾部插入,可以看出: 在表尾插入结点,效率很低,需要遍历整个链表才能插入。
*/
public void insertLast(T t) {
Node<T> newNode = new Node<T>(t);
Node<T> p = head;
while (p.next != null) {
p = p.next;
}
p.next = newNode;
newNode.next = null;
}
/**
* 在指定位置前插入一个新结点 插入操作可能有四种情况:
* ①表为空, 返回false
* ②表非空,指定的数据不存在
* ③指定的数据是表的第一个元素
* ④指定的数据在表的中间
*
* @param known 指定的已知节点
* @param t 要插入的结点
*/
public boolean insertBefore(T known, T t) {
Node<T> prev = head, curr = head.next, newNode;
newNode = new Node<T>(t);
if (!isEmpty()) { // 非空
while ((curr != null) && (!known.equals(curr.nodeValue))) { // 两个判断条件不能换
prev = curr;
curr = curr.next;
}
newNode.next = curr;
prev.next = newNode;
return true;
}
return false; // 表空
}
获取元素,
1、直接获取表头元素,时间复杂度为常数,获取成功返回元素,失败则表示链表为空,抛出异常。
2、已知元素,获取元素所在的位置,需要遍历链表时间复杂度为O(n),若获取成功,返回元素位置,失败返回-1。
<span style="white-space:pre"> </span>/**
* 取得链表的表头存储的数据
*/
public Node<T> getFirst() {
if (isEmpty()) {
throw new RuntimeException("链表为空!");
}
return head;
}
/**
* 返回此列表中首次出现的指定元素的索引,如果列表中不包含此元素,则返回 -1.
*/
public int indexOf(T t) {
int index = 1; // 从1开始计算
Node<T> p;
for (p = head; p != null; p = p.next) {
if (t.equals(p.nodeValue))
return index;
index++;
}
return -1;
}
删除: 同插入一样,也分为3中情况
1、 在表头删除,直接将表头指针指向下一个节点,效率非常高,时间复杂度为O(n)。
2、 在表尾删除,效率很低 ,需要遍历整个链表找到最后节点,然后删除节点,将倒数第二个节点的next引用赋值为空。由于需要遍历,所以时间复杂度为O(n)。
3、 删除指定节点, 效率同样非常低,需要遍历链表找到第一次出现的指定节点,然后进行删除,所以时间复杂度为O(n)。
/**
* 在表头删除结点,效率非常高
*/
public Node<T> removeFirst() {
if (isEmpty()) {
throw new RuntimeException("链表为空,不能删除表头!");
}
Node<T> temp = head;
head = head.next;
return temp;
}
/**
* 在表尾删除结点,效率很低 ,需要遍历整个链表找到最后节点。
* 此时双端链表和但链表一样不利于删除最后一个链节点,因为没有一个引用指向倒数第二个链节点
* 如果最后一个节点被删除,倒数第二个节点的next 字段变成空。
*/
public Node<T> removeLast() {
Node<T> previous = null, current = head;
while (current.next != null) {
previous = current;
current = current.next;
if (current.next == null) {
previous.next = null;
}
}
return current;
}
/**
* 移除此列表中首次出现的指定元素 删除操作可能出现的情况:
* ①previous为空,这意味着current为head. head = curr.next; --> removeFirst();
* ②匹配出现在列表中的某个中间位置,此时执行的操作是 --> prev.next = curr.next;,
* 在列表中定位某个结点需要两个引用:一个对前一结点(previous左)的引用以及一个对当前结点(current右)的引用.
* previous = current; current = curr.next;
*/
public Node<T> remove(T t) {
Node<T> current = head, previous = null;
boolean found = false;
while (current != null && !found) {
if (t.equals(current.nodeValue)) {
if (previous == null)
removeFirst();
else
previous.next = current.next;
found = true;
} else {
previous = current;
current = current.next;
}
}
return current;
}
其他方法:
判空、获取链表大小...
/**
* 判断链表是否为空
*/
public boolean isEmpty() {
return head == null;
}
/**
* 取得链表的大小
*/
public int getSize() {
int count = 0;
Node<T> p;
for (p = head; p != null; p = p.next) {
count++;
}
return count;
}
/**
* 如果此列表包含指定元素,则返回 true。
*/
public boolean contains(T t) {
return indexOf(t) != -1;
}
/**
* 打印列表
*/
public void printList() {
if (isEmpty()) {
throw new RuntimeException("链表为空!");
} else {
for (Node<T> p = head; p != null; p = p.next)
System.out.print(p.nodeValue + " ");
}
}
从上一节实现的 顺序表以及这节的 单链表可以看出它俩之间的区别以及优缺点:
顺序表:1、 插入和删除操作需要移动大量元素,等概率情况下顺序表不利于插入特别是在表头插入和删除时。2、顺序表中存储元素的数组容量难以确定,虽然在 MyArrayList中在数组满的时候扩展了数组,但是还是难以确定顺序表大小,可能不够更可能引起浪费,从而引起了存储空间的浪费。 3、虽然顺序表不利于插入删除,但是顺序表中查找是十分高效的,已知元素位置,可以直接返回元素。
链表: 1、链表中查找必须遍历真个链表,效率不高。 2、链表中删除虽然同样需要查找删除位置,但是不需要移动大量元素,在给出指向链表中的某个合适位置的指针后,插入和删除所需时间为O(1)。 3、链表不必要求存储空间的连续性。
关于双链表,笔者将在下一节进行介绍。
全部代码以及测试
package org.TT.LinkedList;
public class SingleLinkedList<T> {
// 下面是SingleLinkedList类的数据成员和方法 head 指向表头
private Node<T> head;
public SingleLinkedList() {
head = null;
}
/**
* 在表头插入结点,效率非常高,由于有对表头的引用
*/
public void insertFirst(T t) {
Node<T> newNode = new Node<T>(t);
if (isEmpty()) {
head = newNode; // 链表为空为空,将head指向 newNode
} else {
newNode.next = head;
head = newNode;
}
}
/**
* 这个是没有尾指针时,在链表尾部插入,可以看出: 在表尾插入结点,效率很低,需要遍历整个链表才能插入。
*/
public void insertLast(T t) {
Node<T> newNode = new Node<T>(t);
Node<T> p = head;
while (p.next != null) {
p = p.next;
}
p.next = newNode;
newNode.next = null;
}
/**
* 在指定位置前插入一个新结点 插入操作可能有四种情况:
* ①表为空, 返回false
* ②表非空,指定的数据不存在
* ③指定的数据是表的第一个元素
* ④指定的数据在表的中间
*
* @param known 指定的已知节点
* @param t 要插入的结点
*/
public boolean insertBefore(T known, T t) {
Node<T> prev = head, curr = head.next, newNode;
newNode = new Node<T>(t);
if (!isEmpty()) { // 非空
while ((curr != null) && (!known.equals(curr.nodeValue))) { // 两个判断条件不能换
prev = curr;
curr = curr.next;
}
newNode.next = curr;
prev.next = newNode;
return true;
}
return false; // 表空
}
/**
* 取得链表的表头存储的数据
*/
public Node<T> getFirst() {
if (isEmpty()) {
throw new RuntimeException("链表为空!");
}
return head;
}
/**
* 返回此列表中首次出现的指定元素的索引,如果列表中不包含此元素,则返回 -1.
*/
public int indexOf(T t) {
int index = 1; // 从1开始计算
Node<T> p;
for (p = head; p != null; p = p.next) {
if (t.equals(p.nodeValue))
return index;
index++;
}
return -1;
}
/**
* 在表头删除结点,效率非常高
*/
public Node<T> removeFirst() {
if (isEmpty()) {
throw new RuntimeException("链表为空,不能删除表头!");
}
Node<T> temp = head;
head = head.next;
return temp;
}
/**
* 在表尾删除结点,效率很低 ,需要遍历整个链表找到最后节点。
* 此时双端链表和但链表一样不利于删除最后一个链节点,因为没有一个引用指向倒数第二个链节点
* 如果最后一个节点被删除,倒数第二个节点的next 字段变成空。
*/
public Node<T> removeLast() {
Node<T> previous = null, current = head;
while (current.next != null) {
previous = current;
current = current.next;
if (current.next == null) {
previous.next = null;
}
}
return current;
}
/**
* 移除此列表中首次出现的指定元素 删除操作可能出现的情况:
* ①previous为空,这意味着current为head. head = curr.next; --> removeFirst();
* ②匹配出现在列表中的某个中间位置,此时执行的操作是 --> prev.next = curr.next;,
* 在列表中定位某个结点需要两个引用:一个对前一结点(previous左)的引用以及一个对当前结点(current右)的引用.
* previous = current; current = curr.next;
*/
public Node<T> remove(T t) {
Node<T> current = head, previous = null;
boolean found = false;
while (current != null && !found) {
if (t.equals(current.nodeValue)) {
if (previous == null)
removeFirst();
else
previous.next = current.next;
found = true;
} else {
previous = current;
current = current.next;
}
}
return current;
}
/**
* 判断链表是否为空
*/
public boolean isEmpty() {
return head == null;
}
/**
* 取得链表的大小
*/
public int getSize() {
int count = 0;
Node<T> p;
for (p = head; p != null; p = p.next) {
count++;
}
return count;
}
/**
* 如果此列表包含指定元素,则返回 true。
*/
public boolean contains(T t) {
return indexOf(t) != -1;
}
/**
* 打印列表
*/
public void printList() {
if (isEmpty()) {
throw new RuntimeException("链表为空!");
} else {
for (Node<T> p = head; p != null; p = p.next)
System.out.print(p.nodeValue + " ");
}
}
public static void main(String[] args) {
SingleLinkedList<String> t = new SingleLinkedList<String>();
t.insertFirst("B");
t.insertFirst("A");
t.insertLast("C");
t.insertLast("D");
t.insertLast("E");
t.insertLast("F");
t.insertBefore("B", "TT"); // 在B前面插入
t.printList();
System.out.println();
System.out.println("‘C’ 在链表的位置:" + t.indexOf("C")); // 寻找 C的位置并且返回位置为 4
//若返回 -1 则表示没有找到
System.out.println("是否含有‘A’?======" + t.contains("A")); // 含有A 返回 true
System.out.println("删除 最后一个 、 第一个 、 和 ‘D’");
t.removeLast();
t.removeFirst();
t.remove("D");
System.out.print("删除后: ");
t.printList();
}
}
节点类
package org.TT.LinkedList;
/**
* 单链表时Node 存储;两个变量: nodeValue next
* 这里没有设置Node 的属性为 private 主要是为了其他类访问方便,若要进行保护,可以提供相关的get set方法进行访问
*
*/
public class Node<T> {
T nodeValue; // 数据域
Node<T> next; // 指针域保存着下一节点的引用
public Node(T nodeValue, Node<T> next) {
this.nodeValue = nodeValue;
this.next = next;
}
public Node(T nodeValue) {
this(nodeValue, null);
}
}<strong>
</strong>
测试结果