线性表(二) — 单链表
单链表的定义
链式存储结构的特点: 用一组任意的存储单元来存储线性表的数据元素(这组存储单元可以地址连续,也可以地址不连续);为了让地址不连续的元素也能在逻辑关系上保持相连,那么,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置);
节点(node): 由元素数据本身和指向后继的信息组成;其中,存储元素数据本身的域我们称之为数据域(data);存储指向后继信息的域我们称之为指针域(next);指针域通常称为指针或链;
单链表: n个节点链结成一个链表,即为线性表的链式存储结构;又由于此链表中的每个节点只有一个指针域,所以称之为线性链表或单链表;
注意:
- 整个链表的存取必须从头指针开始,头指针指向单链表的第一个节点(首元节点);
- 由于最后一个数据元素没有直接后继,所以最后一个节点的指针为null;
- 用单链表表示线性表时,每个数据元素之间的逻辑关系是由节点中的指针来指示的;
- 在使用单链表时,只需要关注数据元素之间的逻辑关系,不需要去关注物理存储地址;
单链表的实现
单链表的初始化:
-
目的:创建并初始化头结点,并让其指针域为空;
/** * 单链表的头节点 */ private Node<E> first; /** * 临时节点 */ private Node<E> temp; /** * 存储单链表中的元素个数 */ private int size = 0; /** * 初始化头结点,让头节点的指针域为空 */ public LinkList() { first = new Node<>(); first.next = null; }
添加元素:
-
在单链表末尾加入元素:
/** * 向单链表的末尾加入元素 * @param element 要加入的元素 */ public void add(E element) { // 创建新节点 Node<E> node = new Node<>(element, null); temp = first; if (temp != null) { // 让temp遍历至末尾元素 for (int i = 0; i < size; i++) { temp = temp.next; } } temp.next = node; // 让末尾元素的节点指向新加入的节点 size ++; }
-
在指定索引位置加入元素:
/** * 在指定的位置插入给定的元素 * @param index 要插入的位置 * @param element 要插入的元素 */ public void insert(int index, E element) { // 检查索引 checkIndex(index); // 创建新的节点 Node<E> node = new Node<>(element, null); temp = first; // 临时节点 if (temp != null) { // 遍历到要插入的位置 for (int i = 0; i < index; i++) { temp = temp.next; } node.next = temp.next; // 新元素的节点指向后一个节点 temp.next = node; // 前一个节点指向新节点 size ++; } }
- 合并链表:
/**
* 将两个单链表连接起来
* @param list1
* @param list2
* @return
*/
public LinkList<E> connect(LinkList<E> list1,
LinkList<E> list2) {
// 获取list1的最后一个节点
Node end = getNode(list1.size - 1);
// 让list1的最后一个节点指向list2的首节点
end.next = list2.getNode(0);
// list1 size增加
list1.size += list2.size;
return list1;
}
取值操作:
-
根据索引取元素:
/** * 获取指定索引位置的元素 * @param index 需要获取的索引 * @return 返回查到的元素值 */ public E getElement(int index) { checkIndex(index); temp = getNode(index); // 把相同步骤封装了,后面会给到代码 return temp.data; }
-
根据元素返回索引:
/** * 根据给定的元素返回对应的索引 * @param element 需要查找的元素 * @return 返回索引值,查找失败返回-1 */ public int getIndex(E element) { temp = first; if (temp != null) { // 遍历单链表并与每个元素进行比较 for (int i = 0; i < size; i++) { temp = temp.next; if (temp.data.equals(element)) { return i; } } } return -1; }
修改元素:
/**
* 将指定索引位置的元素替换成给定的元素
* @param index 指定的索引位置
* @param element 需要替换成的元素
*/
public void set(int index, E element) {
checkIndex(index);
temp = getNode(index);
temp.data = element;
}
删除元素:
-
根据索引删除元素:
/** * 删除指定索引位置的元素 * @param index 删除元素的索引位置 */ public void delete(int index) { checkIndex(index); if (index == 0) { temp = first; temp.next = temp.next.next; } else { getNode(index - 1); temp.next = temp.next.next; } size --; }
-
根据元素值删除:
/** * 删除给定的元素 * @param element 需要删除的元素 */ public void remove(E element) { int index = getIndex(element); // 根据元素值找到其对应的索引值 delete(index); // 调用根据索引删除元素 }
辅助方法:
/**
* 检查索引是否越界
* @param index 需要检查的索引
*/
public void checkIndex(int index) {
if(index < 0 || index >= size) {
throw new RuntimeException("索引不合法:" + index);
}
}
/**
* 将遍历链表封装起来
* @param index
*/
private Node getNode(int index) {
temp = first;
if (temp != null) {
temp = temp.next;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
}
return temp;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
Node temp = first;
if (size == 0) {
return "[]";
} else {
temp = temp.next;
for (int i = 0; i < size; i++) {
sb.append(temp.data).append(",");
temp = temp.next;
}
}
sb.setCharAt(sb.length() - 1, ']');
return sb.toString();
}
Node节点:
/**
* 单链表中的节点
*/
private static class Node<E> {
E data;
Node next;
public Node(){}
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
}
单链表的缺点
缺点: 所有操作的时间复杂度都为O(n)
原因: 因为单链表没有索引,要找到指定位置的元素时只能从头开始遍历,直到到达该索引位置,才能获取对应的元素;
循环链表
定义: 循环链表(Circular Linked List)是另一种形式的链式存储结构(其实它是一种特殊的单链表);
特点: 表中的最后一个节点的指针域指向头节点,整个链表形成一个环;
循环链表的实现
循环链表的实现与单链表大致相同,只是在末尾加入的方法有所不同,因为遍历到尾节点时结束的条件不再是temp.next = null
,而是temp.next.equals(first)
尾部加入操作:
/**
* 向单链表的末尾加入元素
* @param element 要加入的元素
*/
public void add(E element) {
// 创建新节点
Node<E> node = new Node<>(element, null);
temp = first;
// 判断是否是第一次加入
if (size == 0) {
temp.next = node; // 头节点指向新加入元素
} else {
// 如果不是第一次加入就遍历找到尾节点,尾节点指向头指针;
while (!temp.next.equals(first)) {
temp = temp.next;
}
temp.next = node; // 尾节点指向新的节点
}
node.next = first; // 新节点指向头结点
size ++;
}