由来
各位小伙伴们好呀!本人最近看了下《啊哈算法》,写的确实不错。
但稍显遗憾的是,书籍示例代码是c语言,而不是本人常用的Java。
那就弥补遗憾,说干就干,把这本书的示例语言用java写一遍, 顺带附上一些自己的理解!
今天这篇博客讲的是链表。
来不及买纸质书但又想尽快感受算法魅力的童鞋也甭担心,电子版的下载链接已经放到下方了,可尽情下载。
链接:https://pan.baidu.com/s/1imxiElcCorw2F-HJEnB-PA?pwd=jmgs
提取码:jmgs
代码地址
本文代码已开源:
git clone https://gitee.com/guqueyue/my-blog-demo.git
请切换到gitee分支,
然后查看aHaAlgorithm模块下的src/main/java/com/guqueyue/aHaAlgorithm/chapter_2_StackAndChainTable
即可!
数组 vs 链表
在之前的两期博客中,我们都是用数组来模拟队列和栈:
当然,我们用链表同样可以模拟队列和栈。
数组和链表可以说是两种最基本的数据结构了。那么,什么是数组?
数组可以说是一段连续的存储空间。
如图:
而链表是一种离散的存储结构,链表不要求连续,从而可以更好的利用磁盘的存储空间。
链表的每一个元素节点都包括下一个元素节点的地址,如果是尾节点,则下一个元素节点的地址为空。
因此,链表寻找元素就好比找人,就像电视剧《佛陀传》中 耶输陀罗公主 想要找到 悉达多王子,她首先在皇宫问了下 女仆曼阇利卡,然后知道了马厩,又在马厩问了马夫车匿,才知道悉达多在山上的树下,从而找到了悉达多王子。
如图:
这样一看,链表似乎灵活的多,数组则显得有点呆。那么,是这样吗?我们来具体分析一下:
一般我们对元素的操作可以分为两类:查找和更新,而更新又可分为插入、删除、修改。
在占用空间是一样的前提下,衡量数据结构的好坏当然是执行速率。数组和链表则是各擅胜场:
查找、修改元素
数组更快。数组是连续的,而链表还需要一个一个的获取下一个元素的地址,然后才能获取或者修改元素。插入、删除元素
链表更快。链表只需要改动部分元素节点,而数组则需要整体移动更改。
就像作者书中说的那样,如果要在数组中插入一个数,需要整体后移:
而链表只需要简单改动:
链表
创建
那么,我们要如何实现链表呢?
在书中,因为作者用的是c语言,需要用到指针,所以作者特意讲解了下指针。
指针这东西确实让人有些头疼。但所幸,咱们用的是Java,Java里面没有指针,所以我们直接跳过指针。
要通过代码实现链表的话,首先我们需要创建一个节点类用来表示链表的节点:
package com.guqueyue.aHaAlgorithm.entity;
/**
* @Author: guqueyue
* @Description: 链表类
* @Date: 2024/1/15
**/
public class Node {
public int data; // 数据
public Node next; // 下一个数据对象
/**
* 无参构造方法
**/
public Node() {
}
/**
* 全参构造方法
**/
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
然后我们可以创建一个方法用于创建链表。在这个方法中,我们通过遍历的方式逐个给链表的节点赋值,并且将链表的头节点返回:
/**
* @Description 创建一个链表,并返回头节点
* @Param []
* @return com.guqueyue.aHaAlgorithm.entity.ChainTable
**/
private static Node createChainTable() {
System.out.print("您希望输入几个数:");
int n = scanner.nextInt();
System.out.print("请输入(以空格分开,再按回车即可):");
Node head = null; // 声明头节点
Node curr = new Node(); // 声明当前节点
for (int i = 0; i < n; i++) {
// 创建下一个节点
Node next = new Node(scanner.nextInt(), null);
if (head == null) {
head = next; // 如果头节点为空,则头节点指向下一个节点
}else {
curr.next = next; // 如果头节点不为空,则当前节点的下一个节点指向下一个节点
}
curr = next; // 给下一个节点赋值
}
// 返回头节点
return head;
}
遍历
为了方便验证我们的成果,我们需要一个遍历打印链表的方法:
/**
* @Description 打印链表
* @Param [head:头节点]
* @return void
**/
private static void printChainTable(Node head) {
Node temp = head; // 声明临时节点并赋值
System.out.print("链表为:" + temp.data); // 打印头节点
while (temp.next != null) {
// 获取下一个节点并打印
temp = temp.next;
System.out.print("->" + temp.data);
}
System.out.println(); // 换行
}
下面我们来创建一个链表,并且遍历打印试试:
package com.guqueyue.aHaAlgorithm.chapter_2_StackAndChainTable;
import com.guqueyue.aHaAlgorithm.entity.Node;
import java.util.Scanner;
/**
* @Author: guqueyue
* @Description: 链表模拟
* @Date: 2024/1/15
**/
public class ChainTableTest {
// 创建一个Scanner对象, 用于控制台输入
public static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
// 创建一个链表,并返回头节点
Node head = createChainTable();
// 打印链表
printChainTable(head);
}
/**
* @Description 打印链表
* @Param [head:头节点]
* @return void
**/
private static void printChainTable(Node head) {
Node temp = head; // 声明临时节点并赋值
System.out.print("链表为:" + temp.data); // 打印头节点
while (temp.next != null) {
// 获取下一个节点并打印
temp = temp.next;
System.out.print("->" + temp.data);
}
System.out.println(); // 换行
}
/**
* @Description 创建一个链表,并返回头节点
* @Param []
* @return com.guqueyue.aHaAlgorithm.entity.ChainTable
**/
private static Node createChainTable() {
System.out.print("您希望输入几个数:");
int n = scanner.nextInt();
System.out.print("请输入(以空格分开,再按回车即可):");
Node head = null; // 声明头节点
Node curr = new Node(); // 声明当前节点
for (int i = 0; i < n; i++) {
// 创建下一个节点
Node next = new Node(scanner.nextInt(), null);
if (head == null) {
head = next; // 如果头节点为空,则头节点指向下一个节点
}else {
curr.next = next; // 如果头节点不为空,则当前节点的下一个节点指向下一个节点
}
curr = next; // 给下一个节点赋值
}
// 返回头节点
return head;
}
}
运行得:
插入
按照书上的例子,我们再来写一个往链表中按照元素顺序为从小到大 插入节点的方法:
/**
* @Description 往链表中插入数据
* @Param [head: 头节点]
* @return void
**/
private static Node insertChainTable(Node head) {
// 插入
System.out.print("请输入要插入的数: ");
int num = scanner.nextInt();
Node t = head; // 声明临时节点表示当前节点
while (t != null) {
if (t.next != null && t.next.data > num) { // 如果下一个节点的值大于插入数,则插入
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, t.next);
t.next = newNode; // 当前节点的下一个节点指向新增节点
break; // 结束
}
t = t.next; // 当前节点指向下一个节点
}
return head;
}
按照这个方法,我们插入后再来打印链表验证一下(完整代码已开源,在本博客最后也可查看):
public static void main(String[] args) {
// 创建一个链表,并返回头节点
Node head = createChainTable();
// 打印链表
printChainTable(head);
// 往链表中插入数据
head = insertChainTable(head);
// head = insertChainTable2(head);
// 再次打印链表
printChainTable(head);
}
运行得:
这样看来好像并没有什么问题,但是多观察尝试你会发现作者的逻辑存在两个比较明显的问题:
- 如果插入的节点值大小小于头节点,该节点会被插入到头节点后面,违背了从小到大的顺序。
- 如果插入的节点值大于等于尾结点,则该节点不会被插入。
如:
又比如:
因此,我们来改进一下:
/**
* @Description 往链表中插入数据
* @Param [head: 头节点]
* @return void
**/
private static Node insertChainTable2(Node head) {
// 插入
System.out.print("请输入要插入的数: ");
int num = scanner.nextInt();
if (num <= head.data) { // 如果插入的节点值小于等于头节点的值,则直接插入为头节点
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, head);
head = newNode;
}else {
Node t = head; // 声明临时节点表示当前节点
while (t != null) {
if (t.next == null) { // 如果下一个节点为空,则说明是尾节点,并且插入的节点值为最大值,直接插入为尾节点
t.next = new Node(num, null);
break;
} else if(t.next.data >= num) { // 如果下一个节点的值大于插入数,则插入
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, t.next);
t.next = newNode; // 当前节点的下一个节点指向新增节点
break; // 结束
}
t = t.next; // 当前节点指向下一个节点
}
}
return head;
}
再改动一下代码:
public static void main(String[] args) {
// 创建一个链表,并返回头节点
Node head = createChainTable();
// 打印链表
printChainTable(head);
// 往链表中插入数据
// head = insertChainTable(head);
head = insertChainTable2(head);
// 再次打印链表
printChainTable(head);
}
运行代码,控制台输入,可得:
以及
搞定!!!
完整代码
完整代码如下:
package com.guqueyue.aHaAlgorithm.chapter_2_StackAndChainTable;
import com.guqueyue.aHaAlgorithm.entity.Node;
import java.util.Scanner;
/**
* @Author: guqueyue
* @Description: 链表模拟
* @Date: 2024/1/15
**/
public class ChainTableTest1 {
// 创建一个Scanner对象, 用于控制台输入
public static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
// 创建一个链表,并返回头节点
Node head = createChainTable();
// 打印链表
printChainTable(head);
// 往链表中插入数据
// head = insertChainTable(head);
head = insertChainTable2(head);
// 再次打印链表
printChainTable(head);
}
/**
* @Description 往链表中插入数据
* @Param [head: 头节点]
* @return void
**/
private static Node insertChainTable(Node head) {
// 插入
System.out.print("请输入要插入的数: ");
int num = scanner.nextInt();
Node t = head; // 声明临时节点表示当前节点
while (t != null) {
if (t.next != null && t.next.data > num) { // 如果下一个节点的值大于插入数,则插入
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, t.next);
t.next = newNode; // 当前节点的下一个节点指向新增节点
break; // 结束
}
t = t.next; // 当前节点指向下一个节点
}
return head;
}
/**
* @Description 往链表中插入数据
* @Param [head: 头节点]
* @return void
**/
private static Node insertChainTable2(Node head) {
// 插入
System.out.print("请输入要插入的数: ");
int num = scanner.nextInt();
if (num <= head.data) { // 如果插入的节点值小于等于头节点的值,则直接插入为头节点
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, head);
head = newNode;
}else {
Node t = head; // 声明临时节点表示当前节点
while (t != null) {
if (t.next == null) { // 如果下一个节点为空,则说明是尾节点,并且插入的节点值为最大值,直接插入为尾节点
t.next = new Node(num, null);
break;
} else if(t.next.data >= num) { // 如果下一个节点的值大于插入数,则插入
// 给插入节点赋值,并将插入节点的下一个节点 指向 当前节点的下一个节点
Node newNode = new Node(num, t.next);
t.next = newNode; // 当前节点的下一个节点指向新增节点
break; // 结束
}
t = t.next; // 当前节点指向下一个节点
}
}
return head;
}
/**
* @Description 打印链表
* @Param [head:头节点]
* @return void
**/
private static void printChainTable(Node head) {
Node temp = head; // 声明临时节点并赋值
System.out.print("链表为:" + temp.data); // 打印头节点
while (temp.next != null) {
// 获取下一个节点并打印
temp = temp.next;
System.out.print("->" + temp.data);
}
System.out.println(); // 换行
}
/**
* @Description 创建一个链表,并返回头节点
* @Param []
* @return com.guqueyue.aHaAlgorithm.entity.ChainTable
**/
private static Node createChainTable() {
System.out.print("您希望输入几个数:");
int n = scanner.nextInt();
System.out.print("请输入(以空格分开,再按回车即可):");
Node head = null; // 声明头节点
Node curr = new Node(); // 声明当前节点
for (int i = 0; i < n; i++) {
// 创建下一个节点
Node next = new Node(scanner.nextInt(), null);
if (head == null) {
head = next; // 如果头节点为空,则头节点指向下一个节点
}else {
curr.next = next; // 如果头节点不为空,则当前节点的下一个节点指向下一个节点
}
curr = next; // 给下一个节点赋值
}
// 返回头节点
return head;
}
}