本篇博文通过Java模拟一个单向链表
本篇博文通过Java模拟一个能够实现:头插、尾插、指定插入、修改、删除、打印、反转、反转打印、链表合并等功能的带头节点的单向链表;
文章目录
创建数据域内的存储类User
创建用来存储在单向链表的data域内的类型User
,代码如下:
package edu.hebeu;
/**
* @author 13651
*
*/
public class User {
private int id;
private String name;
private Character sex;
public User(int id, String name, Character sex) {
super();
this.id = id;
this.name = name;
this.sex = sex;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Character getSex() {
return sex;
}
public void setSex(Character sex) {
this.sex = sex;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", sex=" + sex + "]";
}
}
创建用于构成单向链表的节点类Node
该类有两种类型(用来作为data域和next域),data域是主要用来存储上面的User类
,next域是Node类型的,用来指向下一个Node
,代码如下:
package edu.hebeu;
/**
* 节点类
* @author 13651
*
*/
public class Node {
/**
* 数据域
*/
public User data;
/**
* next域
*/
public Node next;
public Node(User data) {
this.data = data;
}
}
单向链表的构建和功能完善
须知:
- 链表是以节点的方式来存储,是链式存储;
- 每个节点包含 data 域, next 域:指向下一个节点.;
- 如图:发现链表的各个节点不一定是连续存储.;
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定;
链表在内存中的存储图解如下所示:
这里我先把代码考出来,在下面会对各个方法进行讲解,SingleLinkedList类
的代码如下:
package edu.hebeu.single;
import java.util.Stack;
import edu.hebeu.Node;
/**
* 单向链表
* @author 13651
*
*/
public class SingleLinkedList {
private Node head;
public SingleLinkedList() {
head = new Node(null); // 头节点不存放数据
}
/**
* 这个方法用来获取链表的头节点
* @return
*/
public Node getHead() {
return head;
}
/**
* 这个方法用来通过头插法添加节点,思路分析:
* 1、将“待插入”节点的next域 指向 头节点的next域(头结点的下一个结点);
* 2、将头节点的next域 指向 “待插入”节点
* @param node “待插入”结点
*/
public void headInsert(Node node) {
node.next = head.next; // 将"待插入"节点的next域 指向 头节点的next域(头节点的下一个节点);
head.next = node; // 将头节点的next域 指向"待插入"节点
}
/**
* 这个方法表示通过尾插法添加节点,思路分析:
* 1、先找到链表的最后一个节点;
* 2、让最后一个节点的next域 指向 待插入的结点;
* 3、将待插入的节点的next域 变成null;
* @param node “待插入”结点
*/
public void tailInsert(Node node) {
Node lastNode = head; // 该变量用来保存插入之前链表的最后一个节点,初始值为头节点
while(true) {
if(lastNode.next == null) { // 如果当前节点的next域 为空,表示当前节点就是最后一个节点
lastNode.next = node; // 让当前链表的最后一个节点的next域 指向 "待插入"节点
node.next = null; // 将"待插入"节点的next域 设置为null,表示其为最后一个节点;(这步其实是多余的,Java中引用类型默认为null)
break;
}
lastNode = lastNode.next; // 将节点后移
}
}
/**
* 这个方法表示通过data域的id属性 升序的插入节点
* 1、找到“被插入”节点的前一个节点;
* 2、将“待插入”节点的next域 指向 “被插入”节点的前一个节点的next域
* 3、将 “被插入”节点的前一个结点的next域 指向 “待插入”节点
* @param node “待插入”节点
*/
public void byIdInsert(Node node) {
Node preNode = head; // 这个变量用来保存“被插入”节点的 前一个节点,初始值为头节点
while(true) {
if(preNode.next == null) { // 如果“被插入”节点的 前一个结点的next域 为null,则表示 “被插入”节点是最后一个节点
node.next = preNode.next;
preNode.next = node;
// 以上表示将“待插入”节点 插入 “被插入”节点 之前
break;
} else if(preNode.next.data.getId() > node.data.getId()) { // 如果“被插入”节点的 前一个节点的id 大于 “待插入”节点的id
node.next = preNode.next;
preNode.next = node;
// 以上表示将“待插入”节点 插入 “被插入”节点 之前
break;
} else if(preNode.next.data.getId() == node.data.getId()) { // 如果“被插入”节点的 前一个节点的id 等于 “待插入”节点的id
System.err.println("id:" + node.data.getId() + "存在,不能加入!");
break;
}
preNode = preNode.next; // 将节点后移
}
}
/**
* 该方法用来实现删除节点
* 1、找到“被删除”节点的前一个节点
* 2、将“被删除”节点的前一个节点的next域 指向 “被删除”节点的next域的next域
* 3、释放掉“被删除”节点的内存资源(会自动被Java的垃圾回收器回收掉)
* @param node “待删除”节点
*/
public void delete(Node node) {
if(head.next == null) { // 如果头结点的next域 为null,即头结点为最后一个节点(链表为null)
System.err.println("链表为空!");
return;
}
Node preNode = head; // 这个变量用来保存“被删除”节点的 前一个节点,初始值为头节点
while(true) {
if(preNode.next == null) { // 如果“被删除”节点的 前一个节点的next域 为null,则表示 “被删除”节点是最后一个节点
break;
} else if(preNode.next.data.getId() == node.data.getId()) { // 如果“被删除”节点的 前的节点的id 等于 “待插入”节点的id
preNode.next = preNode.next.next;
// 以上代码表示将“被删除”节点删除
break;
}
preNode = preNode.next; // 节点后移
}
}
/**
* 该方法用来实现链表节点的修改,思路分析:
* 1、根据指定条件查询节点
* 2、将查询到的节点数据修改
* @param node “待修改”节点
*/
public void update(Node node) {
if(head.next == null) { // 如果头节点的next域 为null,即头节点为最后一个节点(链表为null)
System.err.println("链表为空!");
return;
}
Node currentNode = head.next; // 因为头节点不存储数据,所以将头节点的下一个结点设置为当前的节点
while(true) {
if (currentNode == null) { // 如果当前节点为null,即链表已经遍历完了
break;
} else if(currentNode.data.getId() == node.data.getId()) { // 如果当前结点的data域 的id 等于 “待修改”结点的id,即找到了“要修改”结点
currentNode.data.setName(node.data.getName());
currentNode.data.setSex(node.data.getSex());
// 以上代码表示进行修改
break;
}
currentNode = currentNode.next; // 将节点后移
}
}
/**
* 这个方法用来获取链表的倒数第index个节点(新浪面试题),思路分析:
* 1、先获取链表的长度 记为length;
* 2、在判断 倒数第lastIndex 个节点是否合法(lastIndex要大于0,且小于等于链表长度length);
* 3、思路转换:获取倒数的 lastIndex 个节点 就是 获取 第(length - lastIndex)个节点;
* @param lastIndex 倒数第几个
* @return
*/
public Node findNodeLastIndex(int lastIndex) {
if(head.next == null) {
System.err.println("链表为空!");
return null;
}
int length = getLength(); // 获取当前链表的长度
if(lastIndex > length || lastIndex <= 0) { // 如果要查询的倒数的lastIndex是大于链表的长度的 或者 是小于等于0的
System.err.println("未找到倒数第" + lastIndex + "个节点");
return null;
}
Node currentNode = head.next; // 将当前节点初始化为头结点的后一个节点
for(int i = 0; i < length - lastIndex; i++) { // 遍历至倒数第lastIndex个节点
currentNode = currentNode.next; // 将节点后移
}
return currentNode;
}
/**
* 这个方法用来计算链表中有效节点的个数
* @return
*/
public int getLength() {
if(head.next == null) {
System.err.println("链表为空!");
return 0;
}
int length = 0; // 改变量用来保存链表的长度
Node currentNode = head.next; // 将当前节点初始化为头结点的后一个节点
while(true) {
if(currentNode == null) {
break;
}
length++;
currentNode = currentNode.next; // 将节点后移
}
return length;
}
/**
* 这个方法用来实现链表的反转(腾讯面试题),思路分析:
* 1、先定义一个反转后链表的头结点 reversetHead;
* 2、从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放到反转后链表的最前端(即反转后链表头结点之后);
* 3、将原链表的头结点的next域 指向 反转后链表的next域;
*/
public void reverset() {
if(head.next == null || head.next.next == null) { // 如果链表为null 或者 链表只有一个节点
return; // 直接结束该方法,因为此时链表反转和原先的顺序是一样的
}
Node currentNode = head.next; // 这个变量用来保存当前节点,初始值为头结点的后一个节点
Node nextNode = null; // 这个变量用来保存当前节点的下一个节点
Node reversetHead = new Node(null); // 这个变量用来保存反转后链表的头结点
while(currentNode != null) { // 如果当前节点不为空,即链表还没有遍历完
nextNode = currentNode.next; // 保存当前节点在原链表中的下一个节点
currentNode.next = reversetHead.next; // 将当前节点的next域 指向 反转后链表的头结点 即将当前节点添加到最前面
reversetHead.next = currentNode; // 反转后链表的头结点的next域 指向 当前节点
currentNode = nextNode; // 将节点后移
}
head.next = reversetHead.next; // 将原链表的头结点的下一个节点 指向 反转后链表的头结点的下一个节点
}
/**
* 这个方法用来实现有序链表的有序合并(将有序链表source合并至本有序链表对象,合并之后的链表仍是有序的)
* @param source 被合并的有序链表
*/
public void merge(SingleLinkedList source) {
Node sourceHead = source.getHead(); // 获取被合并链表的头节点
if(sourceHead.next == null) { // 如果被合并链表的头节点的next域为null,即表示被合并链表无数据
return;
}
if(head.next == null) { // 如果本链表的头节点的next域为null,即表示本链表无数据
head.next = sourceHead.next; // 让本链表的头节点的next域 指向 被合并链表的next域
return;
}
Node preNode = head; // 用来保存本链表的当前节点的前一个节点
Node sourceCurrentNode = sourceHead.next; // 用来保存被合并链表的当前节点
Node sourceNextNode = null; // 用来保存被合并链表的当前节点的后一个节点
while(sourceCurrentNode != null) { // 如果被合并链表的当前节点不为空,即被合并链表没有遍历完
sourceNextNode = sourceCurrentNode.next; // // 保存被合并链表的当前节点在其原链表中的下一个节点
while(true) { // 如果当前节点不为空,即当前链表还没有遍历完
if(preNode.next == null) { // 如果本链表的当前节点的前一个节点的next域 为null,则表示是本链表的最后一个节点
sourceCurrentNode.next = preNode.next; // 被合并链表的当前节点的next域 指向 本链表的当前节点的前一个节点的next域
preNode.next = sourceCurrentNode; // 本链表的当前节点的前一个节点的next域 指向 被合并链表的当前节点
break;
} else if(preNode.next.data.getId() > sourceCurrentNode.data.getId()) { // 如果本链表的当前节点的前一个节点的id 大于 被合并链表的当前节点的id
sourceCurrentNode.next = preNode.next;
preNode.next = sourceCurrentNode;
// 以上表示将“待插入”节点 插入 “被插入”节点 之前
break;
}
preNode = preNode.next; // 将节点后移
}
sourceCurrentNode = sourceNextNode; // 将被合并链表的节点后移
}
}
/**
* 该方法用来打印链表
*/
public void show() {
if(head.next == null) { // 如果头节点的next域 为null,即头结点为最后一个节点(链表为空)
System.err.println("链表为空!");
return;
}
Node currentNode = head.next; // 因为头节点不存储数据,所以将头节点的下一个节点设置为当前的节点
while(true) {
if(currentNode == null) { // 如果当前节点为null,表示链表已经遍历完了
break;
}
System.out.println(currentNode.data + "->->->"); // 输出当前节点的数据信息
currentNode = currentNode.next; // 将节点后移
}
}
/**
* 从尾到头(倒置)打印单链表(百度面试题),思路分析:
* 1、可以将单链表反转,然后在遍历,但是这样做会破坏原先链表的结构(不推荐);
* 2、可以通过栈的性质(先进后出),将链表的各个节点压入栈,然后再进行弹栈
*/
public void reversetShow() {
if(head.next == null) { // 如果头节点的next域 为null,即头结点为最后一个节点(链表为空)
System.err.println("链表为空!");
return;
}
Stack<Node> nodeStack = new Stack<>(); // 创建Node泛型的栈结构
Node currentNode = head.next; // 因为头节点不存储数据,所以将头节点的下一个节点设置为当前的节点
while(currentNode != null) { // 如果当前节点不为空,即链表还没有遍历完
nodeStack.push(currentNode); // 将当前的节点入栈
currentNode = currentNode.next; // 将节点后移
}
while(nodeStack.size() > 0) { // 如果栈中还有元素
Node node = nodeStack.pop(); // 将栈顶元素出栈
System.out.println(node.data + "->->->"); // 输出当前节点的数据信息
}
}
}
头插法
上述代码的headInsert()方法
就是头插法,头插法的图解如下所示:
尾插法
上述代码的tailInsert()方法
就是尾插法,尾插法的图解如下所示:
在某一位置插入
上述代码的byIdInsert()方法
就是通过链表中的id大小进行升序的插入,在链表的某一位置插入节点图解如下所示:
删除节点
上述代码的delete()方法
就是删除节点,删除链表中的节点图解如下所示:
修改节点
上述代码的update()方法
就是修改某个节点的数据信息,修改链表中的节点图解如下所示:
获取倒数第n个节点
上述代码的findNodeLastIndex()方法
就是获取倒数第n个节点,如下图解:
链表反转
上述代码的reverset()方法
就是单链表的反转,如下图解:
链表的合并
上述代码中的merge()方法
就是有序单链表的有序合并,图解如下所示:
测试类Test的编写
该类主要用来测试上述的链表,Test类
的代码如下:
package edu.hebeu.single;
import java.util.Random;
import java.util.Scanner;
import edu.hebeu.Node;
import edu.hebeu.User;
public class Test {
private static Scanner SCANNER = new Scanner(System.in);
private static int CHOICE;
private static final SingleLinkedList SINGLE_LINKED_LIST = new SingleLinkedList();
public static void main(String[] args) {
while(true) {
menu();
option();
if(CHOICE == 0) {
break;
}
}
System.out.println("bye");
}
public static void menu() {
System.out.println("尾插法添加5条数据,t(tail)");
System.out.println("尾头插法添加5条数据,h(head)");
System.out.println("有序的插入5条数据,o(order)");
System.out.println("修改数据,u(update)");
System.out.println("删除数据,d(delete)");
System.out.println("链表长度,l(length)");
System.out.println("查找链表倒数第n个节点,7(lastIndex)");
System.out.println("反转链表,r(reverset)");
System.out.println("显示链表全部数据,s(show)");
System.out.println("反向显示链表全部数据,1(reversetShow)");
System.out.println("链表合并,2(MergeLinkedList)");
System.out.println("输入其他退出");
char keyword= SCANNER.next().charAt(0);
switch(keyword) {
case 't':
CHOICE = 1;
break;
case 'h':
CHOICE = 2;
break;
case 'o':
CHOICE = 3;
break;
case 'u':
CHOICE = 4;
break;
case 'd':
CHOICE = 5;
break;
case 'l':
CHOICE = 6;
break;
case '7':
CHOICE = 7;
break;
case 'r':
CHOICE = 8;
break;
case 's':
CHOICE = 9;
break;
case '1':
CHOICE = 10;
break;
case '2':
CHOICE = 11;
break;
default:
CHOICE = 0;
break;
}
}
private static void option() {
if(CHOICE == 1) {
Random random = new Random();
for(int i = 0; i < 5; i++) {
User user = new User(random.nextInt(100), "user00" + random.nextInt(10) + i, i / 2 == 0 ? '女' : '男');
Node node = new Node(user);
SINGLE_LINKED_LIST.tailInsert(node);
}
System.out.println("尾插5条数据完毕!");
} else if(CHOICE == 2) {
Random random = new Random();
for(int i = 0; i < 5; i++) {
User user = new User(random.nextInt(100), "user00" + random.nextInt(10) + i, i / 2 == 0 ? '女' : '男');
Node node = new Node(user);
SINGLE_LINKED_LIST.headInsert(node);
}
System.out.println("头插5条数据完毕!");
} else if(CHOICE == 3) {
Random random = new Random();
for(int i = 0; i < 5; i++) {
User user = new User(random.nextInt(100), "user00" + random.nextInt(10) + i, i / 2 == 0 ? '女' : '男');
Node node = new Node(user);
SINGLE_LINKED_LIST.byIdInsert(node);
}
System.out.println("按照id升序插入5条数据完毕!");
} else if(CHOICE == 4) {
System.out.print("请输入要修改的用户的ID:"); int id = SCANNER.nextInt();
System.out.print("请输入要修改的用户的姓名:"); String name = SCANNER.next();
System.out.print("请输入要修改的用户的性别:"); char sex = SCANNER.next().charAt(0);
SINGLE_LINKED_LIST.update(new Node(new User(id, name, sex)));
System.out.println("按照id《" + id + "》修改数据完毕!");
} else if(CHOICE == 5) {
System.out.print("请输入要删除的用户的ID:"); int id = SCANNER.nextInt();
SINGLE_LINKED_LIST.delete(new Node(new User(id, null, null)));
System.out.println("按照id《" + id + "》删除数据完毕!");
} else if(CHOICE == 6) {
int length = SINGLE_LINKED_LIST.getLength();
System.out.println("链表的长度:" + length);
} else if(CHOICE == 7) {
System.out.print("请输入查询倒数第几个节点:");int lastIndex = SCANNER.nextInt();
Node node = SINGLE_LINKED_LIST.findNodeLastIndex(lastIndex); // 获取倒数第lastIndex个节点
System.out.println("倒数第" + lastIndex + "节点的data域:" + (node == null ? "null" : node.data));
} else if(CHOICE == 8) {
SINGLE_LINKED_LIST.reverset();
System.out.println("反转链表成功");
} else if(CHOICE == 9) {
SINGLE_LINKED_LIST.show();
} else if(CHOICE == 10) {
SINGLE_LINKED_LIST.reversetShow();
} else if(CHOICE == 11) {
SingleLinkedList linkedList1 = new SingleLinkedList();
SingleLinkedList linkedList2 = new SingleLinkedList();
while(true) {
System.out.println("链表1有序的添加5条数据,1(1add)");
System.out.println("链表2有序的添加5条数据,2(2add)");
System.out.println("链表1显示数据,3(1show)");
System.out.println("链表2显示数据,4(2show)");
System.out.println("链表2有序的合并至链表1,5(2MergeTo1)");
System.out.print("请输入您的业务:"); int keyword = SCANNER.nextInt();
if(keyword == 1) {
Random random = new Random();
for(int i = 0; i < 5; i++) {
User user = new User(random.nextInt(800), "user00" + random.nextInt(10) + i, i / 2 == 0 ? '女' : '男');
Node node = new Node(user);
linkedList1.byIdInsert(node); // 有序的插入节点
}
System.out.println("linked1有序的添加5条数据完毕");
} else if(keyword == 2) {
Random random = new Random();
for(int i = 0; i < 5; i++) {
User user = new User(random.nextInt(800), "user00" + random.nextInt(10) + i, i / 2 == 0 ? '女' : '男');
Node node = new Node(user);
linkedList2.byIdInsert(node); // 有序的插入节点
}
System.out.println("linked2有序的添加5条数据完毕");
} else if(keyword == 3) {
linkedList1.show();
} else if(keyword == 4) {
linkedList2.show();
} else if(keyword == 5) {
linkedList1.merge(linkedList2);
linkedList2 = new SingleLinkedList(); // 清空链表2中的数据
System.out.println("合并成功!");
} else {
linkedList1 = null;
linkedList2 = null;
break;
}
}
}
}
}