单链表
单链表概述
基本介绍
链表示有序的,但是存储的地址不一定示连续的,存储示意如图下:
简要说明,head头指针指向的是 data域a1,a1的next域指向下一个存储的节点,链表存储的数据并不一定是连续的。
1、链表是以节点的方式存储的
2、每个节点包含data域(存储数据)、next域指向的下一个存储的节点
3、链表的各个节点不一定是连续存放的
4、链表可以有头节点,也可以不包含头节点
逻辑结构示意图如下
单链表实现
实现一个单向链表->完成单链表的增加、删除、修改、查找
代码实现
package com.example.data.sparse.linked;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Objects;
import java.util.UUID;
/**
* @author zjt
* 单链表实现
*/
public class MineSingleLinkedList<E> {
// 头节点 不存放具体数组就是表示单链表的头 也不要动 就表示一个链表的头
Node<E> head = new Node<>(null, null);
// 新增一个节点到单链表
// 1、只在链表的尾部增加
// 2、将最后一个节点的next 指向新的节点
public void add(E e) {
// 需要一个临时变量 temp 用于遍历链表,找到最后一个节点
Node<E> temp = this.head;
// 遍历链表 找到链表的尾节点
while (temp.next != null) {
// 没有找到指针后移
temp = temp.next;
}
temp.next = new Node<>(e);
}
// 指定下标插入
public void add(int index, E e) {
// 头部节点不动 依旧通过辅助变量temp来帮助找到需要添加的位置
// 因为是单链表 所以找到的temp是位于 添加位置的钱一个节点 否则添加会出现问题
Node<E> temp = this.head;
for (int i = 0; i < index; i++) {
if ((temp = temp.next) == null) {
throw new IndexOutOfBoundsException(String.format("Index:%d, Size:%d", index, i));
}
}
// 插入到链表中
temp.next = new Node<>(e, temp.next);
}
// 单链表的修改
// 这里简单实现根据下标修改
public void update(int index, E e) {
if (head.next == null) {
throw new RuntimeException("链表为空");
}
Node<E> temp = head.next;
for (int i = 0; i < index; i++) {
if ((temp = temp.next) == null) {
throw new IndexOutOfBoundsException(String.format("Index:%d, Size:%d", index, i));
}
}
temp.item = e;
}
// 删除节点
// 1、找到待删除节点的前一个节点
// 2、临时变量 e == temp.next.item
// 3、被删除的节点 将不会有其它引用指向,会被垃圾回收机制回收
public void remove(E e) {
if (head.next == null) {
throw new RuntimeException("链表为空");
}
Node<E> temp = head;
while (temp.next != null) {
// 找到待删除节点的前一个节点
if (e.equals(temp.next.item)) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
}
// 显示链表
public void list() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 头节点不要动 需要一个辅助变量遍历
Node<E> temp = head.next;
while (temp != null) {
// 输出节点的信息
System.out.println(temp);
// 将temp后移
temp = temp.next;
}
}
private static class Node<E> { // 静态内部类用于处处数据节点
E item; // 存储的数据
Node<E> next; // 下一个节点的引用
// 构造器
protected Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
public Node(E item) {
this.item = item;
this.next = null;
}
@Override // 显示方便
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
}
@Getter
@Setter
@ToString
//@Data 不使用这个 @Data会重写很多我们不需要的方法 导致调试出现意外
class User {
private Integer id;
private String name;
private String mobile;
public User(Integer id, String name, String mobile) {
this.id = id;
this.name = name;
this.mobile = mobile;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (!Objects.equals(id, user.id)) return false;
if (!Objects.equals(name, user.name)) return false;
return Objects.equals(mobile, user.mobile);
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (mobile != null ? mobile.hashCode() : 0);
return result;
}
}
class Client {
public static void main(String[] args) {
MineSingleLinkedList<User> singleLinkedList = new MineSingleLinkedList<>();
System.out.println("实现尾节点新增");
for (int i = 0; i < 4; i++) {
if (i == 2) {
continue;
}
User user = new User(i, UUID.randomUUID().toString(), "1339890999" + i);
singleLinkedList.add(user);
}
singleLinkedList.list();
// 插入位置3元素
System.out.println("实现插入节点新增");
User user = new User(2, UUID.randomUUID().toString(), "1111");
singleLinkedList.add(2, user);
singleLinkedList.list();
System.out.println("实现指定下标修改");
singleLinkedList.update(0, user);
singleLinkedList.list();
System.out.println("实现equals删除所有");
singleLinkedList.remove(user);
singleLinkedList.list();
}
}
为了使单链表更好用,可以添加更多的额外属性,比如尾节点 tail,链表长度size等属性。
单链表案例
案例一
在上述代码中已经实现的单链表,获取单链表的节点的个数,(如果是头节点仅指向下一个节点,没有存放数据,需要不统计头节点)
// 获取单链表的节点的个数
public int getListSize() {
// 这个例子头节点没有存储数据 不能统计这个头节点
Node<E> temp = head.next;
if (temp == null) {
return 0;
}
int size = 0;
while (temp != null) {
size++;
temp = temp.next;
}
return size;
}
案例二
获取单链表的倒数第n个元素
// 获取单链表的倒数第n个节点
// 思路一
// 1、方法接收一个index 就是倒数n个节点
// 2、先把链表遍历一遍获取链表的长度size
// 3、获取到size后,从链表的第一个开始遍历 size - index个就是链表的倒数第n个节点
// 4、存在节点返回 不存在返回null
public E getLastIndexNode1(int index) {
// 获取链表的长度
assert index > 1;
int size = this.getListSize();
if (head.next == null || size < index) {
return null;
}
int nodeIndex = size - index;
Node<E> temp = head.next;
for (int i = 0; i < nodeIndex; i++) {
temp = temp.next;
}
return temp.item;
}
// 获取单链表的倒数第n个节点
// 思路二
// 1、方法接收一个index
// 2、声明两个临时变量,其中一个遍历index次 第二个变量开始遍历,
// 3、遍历完成第二个临时变量就是倒数第index个元素
public E getLastIndexNode2(int index) {
if (head.next == null) {
return null;
}
assert index > 1; // index 须大于0
Node<E> temp = head.next;
Node<E> temp1 = head.next;
// 分成两个循环写 方便理解
// 1、先将变量 temp 后移index 次
for (int i = 0; i < index; i++) {
if (temp == null) {
return null;
} else {
temp = temp.next;
}
}
// 2、两个变量一起后移 temp后移 直至没有元素
while (temp != null) {
temp = temp.next;
temp1 = temp1.next;
}
return temp1.item;
}
案例三
单链表的反转
// 单链表的反转
// 思路1 新建链表 头插法实现
public void reverseList() {
if (head.next == null || head.next.next == null) {
// 当前链表为空 或者只有一个节点无需反转
return;
}
// 临时变量
Node<E> temp = head.next;
// 清空链表
head = new Node<>(null);
while (null != temp) {
addHeadNode(temp.item);
temp = temp.next;
}
}
// 链表头插法 每次都新增在链表的第一个位置
public void addHeadNode(E e) {
Node<E> temp = head.next;
head.next = new Node<>(e, temp);
}
// 单链表反转
// 思路二 类似于头插法
public void reverseList1() {
if (head.next == null || head.next.next == null) {
// 当前链表为空 或者只有一个节点无需反转
return;
}
Node<E> current = head.next; // 当前节点 辅助指针 用于遍历链表
Node<E> next; // 指向当前节点[current]的下一个节点
Node<E> reverseNode = new Node<>(null); // 设置一个反转的头节点
// 遍历原来的链表 每遍历一个节点 就将其取出并放在新链表的 reverseNode 的最前端
while (current != null) {
next = current.next; // 暂时保存当前节点的下一个节点
current.next = reverseNode.next; // 将current的下一个节点 指向新的链表的最前端
reverseNode.next = current; // 将 反转的头节点下一个节点 指向 当前节点 current 连接到新的链表上
current = next; // 指针后移
}
head.next = reverseNode.next;
}
案例4
逆序打印单链表
// 逆序打印单链表
// 思路一:根据上面实现的单链表反转方法,先反转再打印。缺点:破坏了原有的链表结构
// 思路二:利用递归的方式打印 缺点 因为这里头节点仅作为引用 并不实际存放数据 所以打印会将头节点的null 打印出来
public static <E> void printfListReverseByRecursive(Node<E> head) {
if (head.next != null) {
printfListReverseByRecursive(head.next);
}
System.out.println(head.item);
}
// 思路三 使用栈的特性逆序打印
public void printfListReverseByStack() {
// 空链表 不打印
if (head.next == null) {
return;
}
// 遍历入栈
Stack<E> stack = new Stack<>();
Node<E> temp = head.next;
while (temp != null) {
stack.push(temp.item);
temp = temp.next;
}
// 出栈
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
}
单向链表解决约瑟夫问题
约瑟夫问题,假设编号1,2,3 …n个人,约定号码k(1<=k<=n)的数字,从k的位置开始数,数到m的人出列,下一个人从1开始数,依此类推直到所有人出列,产生一个出队编号。
假定 n =5 有5个人,k=1 第一个位置开始,m=2 每数到2的人出列。
出圈顺序是 2 -> 4 -> 1 -> 5 -> 3
构建一个环形单向链表思路
1、先创建第一个节点,让head指向该节点,并形成环形
2、后面每当我们新增一个节点,就把该节点加入到已有的环形链表中。
遍历环形链表
1、借助辅助遍历temp 指向head节点,然后通过while循环遍历环形链表,temp == head 结束。
约瑟夫问题代码实现
package com.example.data.sparse.linked;
/**
* @author zjt
* 约瑟夫问题
*/
public class Joseph {
public static void main(String[] args) {
RingLinkedList<Integer> list = new RingLinkedList<>();
for (int i = 1; i <= 125 ; i++) {
list.add(i);
}
System.out.println();
list.getJoseph(10,20);
}
}
class RingLinkedList<E> {
// 头节点
Node<E> head;
// 尾节点
Node<E> tail;
// 链表的节点个数
int size = 0;
// 使用尾插法新增数据
// 形成一个环状,让尾节点的next 指向头节点这样就形成一个环状
public void add(E e) {
// 当前的尾节点
Node<E> t = this.tail;
Node<E> newNode = new Node<>(e, null);
// 将新节点作为尾节点
tail = newNode;
if (t == null) { // 新增时链表是空的
// 赋值头节点
head = newNode;
} else {
// 不是空链表
// 将之前的尾节点的next 指向新的节点
t.next = newNode;
}
size++;
// 让尾节点的next 指向头节点
tail.next = head;
}
// 遍历当前循环链表
public void show() {
Node<E> temp = this.head;
if (temp == null) {
System.out.println("空链表");
return;
}
while (true) {
System.out.println(temp.toString());
if (temp.next == head) {
break;
}
temp = temp.next;
}
}
// 约瑟夫环出队问题 1 <= K <= this.size 表示从第几个开始 m 表示数几次出圈
public void getJoseph(int k, int m) {
if (head == null || k < 0 || k > size) {
return;
}
// 创建临时变量 辅助约出圈问题
Node<E> helper = head; // 指向头节点
Node<E> temp = tail; // 指向尾节点 它的下一个节点是头节点
// 出圈报数前先根据需求后移k-1次
// 例如后移2
for (int i = 0; i < k - 1; i++) {
helper = helper.next;
temp = temp.next;
}
// 当元素报数时,temp 和 helper需要后移 m-1次 然后出圈
while (true) {
if (helper == temp) { // 说明圈中仅有一个节点
break;
}
for (int i = 0; i < m - 1; i++) {
temp = temp.next;
helper = helper.next;
}
// 此时temp 指向的节点 就是需要出圈的节点
System.out.println("出圈的节点为:" + helper.item);
// 将temp 指向的节点出圈
helper = helper.next;
temp.next = helper;
}
System.out.println("最后留在圈中的节点:" + helper.item);
}
private static class Node<E> {
E item;
Node<E> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
}
双向链表
概述
基本介绍:
1、双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
与单向链表对比
1、单向链表的查找方向智能从头到尾的方向查找,而双向链表可以从任意一个节点向前或者向后查找。
2、单向链表不能自我删除,需要依靠一个辅助节点(总是需要找到辅助节点的前一个节点来删除),而双向链表可以进行自我删除。
双向链表添加、修改、删除、遍历思路分析
添加(创建) 插尾法
1、先找到双向链表的最后一个节点
2、temp.next = newNode
3、newNode.prev = temp
修改:思路与单向链表基本一致
删除:
1、双向链表可以实现自我删除某个节点
2、直接找到需要删除的节点 delNode
3、delNode.prev.next = delNode.next
4、delNode.next.prev= delNode.prev
遍历:思路与单向链表一致,但是可以向前、向后遍历
package com.example.data.sparse.linked;
import java.util.UUID;
/**
* @author zjt
* 双向链表
* 没有考虑线程安全问题
*/
public class MineBothWayLinkedList<E> {
// 声明一个头节点 跟上面的单链表有所区别 头节点存储数据
Node<E> head;
// 声明一个尾节点 目的是使用起来更加方便
Node<E> tail;
// 这里新增时使用 ++ 删除时使用-- 没有考虑线程安全问题
int size = 0;
public MineBothWayLinkedList() { // 构造器
}
// 尾插法新增节点
public void addTail(E e) {
// 取出尾一个节点
Node<E> t = tail;
// 新建节点
Node<E> newNode = new Node<>(t, e, null);
// 将新节点作为尾节点
tail = newNode;
if (t == null) { // 新增时链表是空的
// 赋值头节点
head = newNode;
} else {
// 不是空链表
// 将之前的尾节点的next 指向新的节点
t.next = newNode;
}
size++;
}
// 头插法新增节点
public void addHead(E e) {
// 取出头节点
Node<E> h = head;
// 新建节点
Node<E> newNode = new Node<>(null, e, h);
// 新节点作为头节点
head = newNode;
if (h == null) { // 头节点为空 证明是空链表
tail = newNode; // 赋值尾节点
} else {
h.prev = newNode; // 将之前的头节点的 前驱节点指向 新的头节点
}
size++;
}
//指定下标新增节点
public void addByIndex(int index, E e) {
if (index > size) {
// 指定下标大于链表长度 抛出异常
throw new IndexOutOfBoundsException(String.format("Size %d; index %d", size, index));
}
if (index == 0) { // 新增头节点
addHead(e);
} else if (index == size) { // 新增尾节点
addTail(e);
} else {
// 找到需要新增节点 的前一个节点(前驱节点)
Node<E> temp = this.head;
// 链表的遍历 性能不高,这里为了省事 就直接循环遍历了 可以优化
for (int i = 1; i < index; i++) {
temp = temp.next;
}
// 新增节点的前驱节点就是 临时变量 temp 后继节点 是 temp.next
Node<E> newNode = new Node<>(temp, e, temp.next);
// 后继节点的 前驱节点 要变更尾 新节点
temp.next.prev = newNode;
// 前驱节点的 后继节点 是新节点
temp.next = newNode;
size++;
}
}
// 删除节点
public void remove(E e) {
if (head == null || e == null) {// 空链表无法移除
return;
}
Node<E> temp = this.head;
while (temp != null) {
if (e.equals(temp.item)) {
// 准备删除该节点
Node<E> prev = temp.prev; //前驱节点
Node<E> next = temp.next; // 后继节点
if (prev == null) { // 前驱节点为空 删除的节点是头节点
head = next; // 当前节点的后继节点作为头节点
} else { // 前驱节点不为空
prev.next = next; // 前驱节点的后继节点指向 当前节点的后继节点
temp.prev = null; // 当前节点与前驱节点断开连接
}
if (next == null) { // 尾节点
tail = prev; // 删除的是尾节点 将当前节点的前驱节点 设置为尾节点
} else {
next.prev = prev;// 后继节点的前驱节点 修改为当前节点的前驱节点
temp.next = null; // 当前节点与后继节点断开连接
}
size--;
temp.item = null; // 移除与具体元素的引用
}
temp = temp.next;
}
}
// 根据下标修改链表元素
public void update(int index, E e) {
if (null == head) { // 空链表 啥也不干
return;
}
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException(String.format("Size %d; index %d", size, index));
}
Node<E> temp = this.head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
temp.item = e;
}
// 显示链表
public void show() {
if (head == null) {
System.out.println("链表为空");
return;
}
// 需要一个辅助变量遍历
Node<E> temp = head;
while (temp != null) {
// 输出节点的信息
System.out.println(temp);
// 将temp后移
temp = temp.next;
}
}
public int getSize() {
return size;
}
private static class Node<E> { // 静态内部类用于处处数据节点
E item; // 存储的数据
Node<E> next; // 下一个节点的引用
Node<E> prev;// 上一个节点的引用
// 构造器
protected Node(Node<E> prev, E item, Node<E> next) {
this.prev = prev;
this.item = item;
this.next = next;
}
@Override // 显示方便
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
}
class Client2 {
public static void main(String[] args) {
MineBothWayLinkedList<User> singleLinkedList = new MineBothWayLinkedList<>();
System.out.println("实现尾节点新增");
for (int i = 0; i < 4; i++) {
User user = new User(i, UUID.randomUUID().toString(), "1339890999" + i);
singleLinkedList.addTail(user);
}
singleLinkedList.show();
// 测试头节点新增
System.out.println("测试头节点新增");
User user = new User(4, UUID.randomUUID().toString(), "13398909994");
singleLinkedList.addHead(user);
singleLinkedList.show();
// 插入新增
System.out.println("插入新增");
User user1 = new User(2001, UUID.randomUUID().toString(), "1339");
singleLinkedList.addByIndex(3, user1);
singleLinkedList.show();
System.out.println(singleLinkedList.getSize());
System.out.println("删除节点");
singleLinkedList.remove(user);
singleLinkedList.show();
System.out.println(singleLinkedList.getSize());
System.out.println("删除节点");
singleLinkedList.remove(user1);
singleLinkedList.show();
System.out.println(singleLinkedList.getSize());
System.out.println("修改节点");
singleLinkedList.update(3, user1);
singleLinkedList.show();
System.out.println(singleLinkedList.getSize());
}
}