数据结构与算法(二)链表
链表
一、概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
二、单链表
- 单链表结点结构如下:
┌───┬───┐
│data │next │data域–存放结点值的数据域;next域–存放结点的直接后继的地址(位置)的指针域(链域)
└───┴───┘
链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的,每个结点只有一个链域的链表称为单链表(Single LinkedList)。
- 头指针head和终端结点
单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。链表由头指针唯一确定,单链表可以用头指针的名字来命名。
终端结点无后继,故终端结点的指针域为空,即NULL。
Java 简单实现
…可以单独设置一个Head节点无数据,仅作为开始标识。
public class SingleList<E> {
//链表的头结点
private Node<E> head;
//链表的尾节点,避免每次插入时遍历单链表
private Node<E> tail;
// 链表的大小
private int size;
/**
* 添加元素
* @param e 要添加的元素
*/
public void addNode(E e) {
if (e == null) {
System.out.println("暂不支持添加null元素");
return;
}
Node<E> node = new Node<>(e);
if (size == 0) {
head = tail = node;
size++;
return;
}
//将新添加的节点加到链表结尾
tail.next = node;
//链表尾指针挪到新添加的节点上
tail = node;
size++;
}
/**
* 删除节点数据
* @param index 索引位置
* @return 删除节点的数据
*/
public E deleteNode(int index) {
if (!checkIndex(index)) {
System.out.println("不是有效索引:" + index);
return null;
}
Node<E> curNode = node(index);
E data = curNode.data;
if (index == 0) {
head = curNode.next;
} else {
//获取前一个位置节点
Node<E> pre = node(index - 1);
pre.next = curNode.next;
}
curNode = null;
size--;
return data;
}
/**
* 更新索引位置节点
* @param index 索引位置
* @param e 节点数据
* @return E 旧的节点数据
*/
public E updNode(int index, E e) {
if (!checkIndex(index)) {
System.out.println("不在有效索引范围");
return null;
}
Node<E> node = node(index);
E data = node.data;
node.data = e;
return data;
}
/**
* 获取index节点数据
* @param index 元素位置
* @return node 节点数据
*/
public E getNode(int index) {
if (!checkIndex(index)) {
System.out.println("不是有效索引:" + index);
return null;
}
return node(index).data;
}
/**
* 根据索引,返回索引位置节点
* @param index 索引
* @return 索引对应节点
*/
Node<E> node(int index) {
Node<E> node = head;
if (index == 0) {
return node;
}
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
/**
* 获取头节点数据
* @return 头结点数据
*/
public E getHead() {
return head.data;
}
/**
* 获取尾节点数据
* @return 尾节点数据
*/
public E getTail() {
return tail.data;
}
/**
* 校验index是否超出当前单链表容量
* <li>true为有效索引</li>
* <li>false为无效索引</li>
* @param index 索引位置
* @return 校验结果
*/
private boolean checkIndex(int index) {
return index >= 0 && index < size;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node<E> node = head;
if (node == null) {
return "当前链表无数据";
}
do {
sb.append("[").append(node.data).append("]");
node = node.next;
}
while (node != null);
return sb.toString();
}
/**
*内部类,表示链表的节点
*/
private static class Node<E> {
E data;
Node<E> next;
Node(E data) {
this.data = data;
}
}
}
三、双向链表
- 双向链表结点结构如下:
┌───┬───┬───┐
│ pre │data │next│
└───┴───┴───┘
data域–存放结点值的数据域;next域–存放结点的直接后继的地址,pre-存放节点的直接前驱的地址
Java简单实现
public class DulList<E> {
// 链表的头结点
private Node<E> head;
// 链表的尾节点
private Node<E> tail;
// 链表的大小
private int size;
/**
* 获取指定索引位置的元素值
*
* @param index 索引位置
* @return 指定索引位置元素值
*/
public E get(int index) {
if (!isElementIndex(index)) {
System.out.println("索引位置无效:" + index);
return null;
}
return node(index).item;
}
/**
* 修改索引对应位置的元素
*
* @param index 索引位置
* @param element 元素
* @return 旧的元素值
*/
public E set(int index, E element) {
if (!isElementIndex(index)) {
System.out.println("索引位置无效:" + index);
return null;
}
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
/**
* 删除指定索引位置的元素并返回元素值
*
* @param index 索引位置
* @return 指定索引位置元素值
*/
public E delete(int index) {
if (!isElementIndex(index)) {
System.out.println("索引位置无效:" + index);
return null;
}
Node<E> node = node(index);
E element = node.item;
Node<E> next = node.next;
Node<E> prev = node.prev;
if (prev == null) {
head = next;
} else {
prev.next = next;
node.prev = null;
}
if (next == null) {
tail = prev;
} else {
next.prev = prev;
node.next = null;
}
node.item = null;
size--;
return element;
}
/**
* 向链表中插入一个元素
*
* @param e 插入的元素
* @return 操作是否成功
*/
public boolean add(E e) {
final Node<E> temp = tail;
final Node<E> newNode = new Node<>(temp, e, null);
tail = newNode;
if (temp == null) {
head = newNode;
} else {
temp.next = newNode;
}
size++;
return true;
}
/**
* 判断索引位置是否在链表中
*
* @param index 索引位置
* @return 判断结果
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 利用二分查找法查找链表中指定索引位置的元素
*
* @param index 索引位置
* @return 索引位置对应的节点
*/
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = head;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = tail;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* 链表中的节点
*/
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
四、循环链表
循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
(1)单循环链表——在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点即可。
(2)多重链的循环链表——将表中结点链在多个环上 。
单循环链表的简单实现
public class CycleSingleList<E> {
//头指针
public Node<E> head;
//尾指针
public Node<E> tail;
//链表长度
int size;
//初始化链表
public CycleSingleList() {
size = 0;
head = new Node<>(null, null);
tail = new Node<>(null, head);
head.next = tail;
}
//判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
/**
* 获取index节点数据
*
* @param index 元素位置
* @return node 节点数据
*/
public E getNode(int index) {
if (!isElementIndex(index)) {
System.out.println("不是有效索引:" + index);
return null;
}
return node(index).data;
}
/**
* 更新索引位置节点
*
* @param index 索引位置
* @param e 节点数据
* @return E 旧的节点数据
*/
public E set(int index, E e) {
if (!isElementIndex(index)) {
System.out.println("不是有效索引:" + index);
return null;
}
Node<E> node = node(index);
E oldVal = node.data;
node.data = e;
return oldVal;
}
/**
* 删除i位置节点,并返回删掉的数据
*
* @param index 索引位置
* @return 删除的数据
*/
public E remove(int index) {
if (!isElementIndex(index)) {
System.out.println("索引位置无效:" + index);
return null;
}
Node<E> curNode = node(index);
E data = curNode.data;
//如果删除的是第一个节点
if (index == 0) {
head = curNode.next;
tail.next=head;
//只有一个元素的情况
if (size==1){
head.data=null;
tail.data=null;
}
} else {
//获取前一个位置节点
Node<E> pre = node(index - 1);
pre.next = curNode.next;
}
curNode = null;
size--;
return data;
}
/**
* 添加元素
*
* @param e 元素值
*/
public void add(E e) {
if (e == null) {
System.out.println("暂不支持添加null元素");
return;
}
Node<E> node = new Node<>(e, null);
if (size == 0) {
head = tail = node;
tail.next = head;
size++;
return;
}
tail.next = node;
node.next = head;
tail = node;
size++;
}
/**
* 判断索引位置是否在链表中
*
* @param index 索引位置
* @return 判断结果
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 根据索引,返回索引位置节点
*
* @param index 索引
* @return 索引对应节点
*/
Node<E> node(int index) {
Node<E> node = head;
if (index == 0) {
return node;
}
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
/**
* 链表节点
*/
private static class Node<E> {
E data;
Node<E> next;
/**
* Node构造函数
*
* @param data 数据域
* @param next next指针
*/
Node(E data, Node<E> next) {
this.data = data;
this.next = next;
}
}
}
五、相关面试题
约瑟夫问题
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
下面是通过上述的循环链表的简单实现:
@Test
void test01() {
StringBuilder result = new StringBuilder();
CycleSingleList<String> list = new CycleSingleList<>();
for (int i = 1; i < 42; i++) {
list.add(String.valueOf(i));
}
CycleSingleList.Node<String> head = list.getHead();
while (list.size >= 3) {
for (int i = 1; i < 3; i++) {
list.tail = list.head;
list.head = list.head.next;
}
String a = list.node(0).data;
list.remove(0);
result.append(a).append("被杀,");
}
System.out.println(result);
//size<3,只剩2个元素
System.out.println(list.head.data);
System.out.println(list.tail.data);
}
两个顺序链表合并成一个顺序链表
大致的思路如下:
新建一个链表存放结果,从A和B中先把头数据取出来比较,将较小的放入新链表中,并将较小的指针挪到下一个位置,然后再次比较。如果两个临时变量都为空则说明比较完了,跳出循环,结束。
比较糙的代码如下:
public static void main(String[] args) {
SingleList<Integer> listA = new SingleList<Integer>();
SingleList<Integer> listB = new SingleList<Integer>();
listA.addNode(1);
listA.addNode(5);
listA.addNode(19);
listA.addNode(22);
listB.addNode(2);
listB.addNode(3);
listB.addNode(7);
listB.addNode(9);
listB.addNode(13);
System.out.println(listA);
System.out.println(listB);
SingleList<Integer> result = merge(listA, listB);
System.out.println(result);
}
/**
* 两个顺序链表合并成一个顺序链表
* @param listA
* @param listB
* @return
*/
private static SingleList<Integer> merge(SingleList<Integer> listA, SingleList<Integer> listB) {
SingleList<Integer> result = new SingleList<Integer>();
Node<Integer> nodeA = listA.head;
Node<Integer> nodeB = listB.head;
while (!(nodeA == null) || !(nodeB == null)) {
if (nodeA == null ) {
result.addNode(nodeB.data);
nodeB = nodeB.next;
} else if ( nodeB == null) {
result.addNode(nodeA.data);
nodeA = nodeA.next;
} else if (nodeA.data >= nodeB.data) {
result.addNode(nodeB.data);
nodeB = nodeB.next;
} else if (nodeA.data < nodeB.data) {
result.addNode(nodeA.data);
nodeA = nodeA.next;
}
}
return result;
}