模拟五个节点,每个节点有两个域:
一个是数据域,存放的是元素
另一个是指针域,存放的是当前节点的下一个节点的地址
1. 第一个 java 文件放的是两个类
一个类实现节点 Node,另一个类实现链表 LinkedList
class Node{
public int val;
public Node next;
public Node (int newval){
this.val = newval;
}
}
public class LinkedList {
//头指针
public Node head;
//初始化链表 ✓
public void initialList(){
Node node1 = new Node(12);
Node node2 = new Node(23);
Node node3 = new Node(34);
Node node4 = new Node(45);
Node node5 = new Node(56);
this.head = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
//此时的node5.next默认就是null
}
//打印链表每个节点存的元素 ✓
public void print(){
Node cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//得到链表的长度 ✓
public int size(){
Node cur = this.head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
//头插法 ✓
public void addFirst(int data){
Node newnode = new Node(data);
if(this.head == null){
this.head = newnode;
return;
}
newnode.next = this.head;
this.head = newnode;
}
//尾插法 ✓
public void addLast(int data){
Node newnode = new Node(data);
if(this.head == null){
this.head = newnode;
return;
}
Node cur = this.head;
while(cur.next != null){
cur = cur.next;
}
cur.next = newnode;
}
//寻找要插入的位置,返回插入节点的前驱 ✓
public Node searchPos(int pos){
int sign = 0;
Node cur = this.head;
while(sign + 1 != pos){
sign++;
cur = cur.next;
}
return cur;
}
//任意位置插入,假设第一个数据节点为 0 号下标 ✓
public void insertData(int pos,int data){
//1. 位置不合法
if(pos<0 || pos>size()){
System.out.println("链表的插入位置不合法!");
}
//2. 头插
else if(pos == 0){
addFirst(data);
}
//3. 尾插
else if(pos == size()){
addLast(data);
}
//4. 中间插
else{
Node newnode = new Node(data);
Node cur = searchPos(pos);
newnode.next = cur.next;
cur.next = newnode;
}
}
//查找是否包含关键字key是否在单链表当中 ✓
public boolean contains(int key){
Node cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}else{
cur = cur.next;
}
}
return false;
}
//删除第一次出现关键字为key的节点 ✓
public void remove(int key){
//1. 链表为空
if(this.head == null){
System.out.println("链表为空,无法删除!");
return;
}
//2. 头删
if(this.head.val == key){
this.head = this.head.next;
return;
}
//3. 中间删 + 尾删
Node prev = this.head;
Node del = this.head;
while(prev.next != null){
if(prev.next.val != key){
prev = prev.next;
}
else{
del = prev;
del.next = del.next.next;
return;
}
}
System.out.println("你所删除的节点在链表中没有找到!");
}
//删除所有值为key的节点 ✓
public void removeAllKey(int key){
//1. 链表为空
if(this.head == null){
System.out.println("链表为空,无法删除!");
return;
}
//2. 开始删除重复的节点
Node cur = this.head;
Node prev = this.head;
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}
else{
prev = cur;
cur = cur.next;
}
}
//3. 考虑头节点
if(this.head.val == key){
this.head = this.head.next;
}
}
//清空链表 ✓
public void clear(){
this.head = null;
}
}
2. 第二个 java 文件放的是一个类 Test
目的是使用主函数来测试单链表的一系列功能
public class Test {
public static void main(String[] args) {
LinkedList test = new LinkedList();
test.initialList();
test.print();
}
}
(1)我所创建的框架是拆解了代码的方法(函数),把类中的每一个方法都拿出来仔细分析。
(2)我画图的地方可能有些不规范,比如地址的表示形式,比如箭头指向等等,我旨在表达逻辑,所以请读者自行理解,也请大佬多多指点。
class Node {
public int val;
public Node next;
}
创建一个类 Node,表示节点,每次插入一个节点,就使用这个类
每个节点有两个元素:
一个存的是当前节点的值 val
另一个存的是下一个节点的地址 next,next 的类型是引用,即当前的 Node.
class Node {
public int val;
public Node next;
public Node(int newval){
this.val = newval;
}
}
public class LinkedList {
public static void main(String[] args) {
Node node1 = new Node(12);
Node node2 = new Node(23);
Node node3 = new Node(34);
}
}
说明:类 Node 中的 public Node(int newval) { },这是构造方法,以便于 LinkedList 中实例化对象,并赋值。
为了展现出清晰的链表结构,我们创建一个新的类 LinkedList,表示整个链表里面的结构,并在这个类中实现各种接口。我们尝试着直接往里面一个一个放数据,为了展示底层原理,我先放在主函数中以帮助理解。
我们为什么不把引用类型 next 置成 null 呢?因为 next 是一个成员变量,如果不将其初始化,那么默认为 null,即不指向任何对象的地址,下图辅助理解。
public class LinkedList {
//头指针
public Node head;
//初始化链表 ✓
public void initialList(){
Node node1 = new Node(12);
Node node2 = new Node(23);
Node node3 = new Node(34);
Node node4 = new Node(45);
Node node5 = new Node(56);
this.head = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
//此时的node5.next默认就是null
}
}
在上述的解释后,我们已经清楚地了解了关于链表的一些基础结构。
那么接下来,在类 LinkedList 中,创建一个方法 initialList,来初始化一个链表,并利用实例化后的对象来将各个节点串起来,并注意,我们要引用一个成员变量 head,以此来记住链表是从哪开始的!而链表的尾部我们不关心,因为它肯定是 null.
从目前来看,这是一个很笨的穷举方法,然而,我们的目的是深入理解链表。
//打印链表每个节点 ✓
public void print(){
Node cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
我们的思想是:遍历链表,当引用指向 null 时,那么就到了最后一个节点。
当然,我们要十分注意,要创建一个新的引用类型 cur,让这个 cur 去跑。因为你如果让 head 去遍历,head 不再指向第一个节点了,以此带来的结果是:我们不再能够控制链表,它就像一个无头苍蝇一样。
此外,我们让代码 cur = cur.next,来不断地拿到下一个节点的地址,以此来遍历,这和 C语言中的指针相似,只是实现方法不同。
下图辅助理解
//得到链表的长度 ✓
public int size(){
Node cur = this.head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
//头插法 ✓
public void addFirst(int data){
Node newnode = new Node(data);
if(this.head == null){
this.head = newnode;
return;
}
newnode.next = this.head;
this.head = newnode;
}
头插法的最关键的思想就是,改变头指针 head 指向的地址。
在没有插入数据之前,head 指向原先的 node1,在插入之后,新的节点 newnode 存放两个数据,一个是整型的 data,一个是空指针 null。
而我们要做的就是让 head 指向 newnode,而 newnode 这个新的节点存放的又是原先的 node1,所以我们通过以下两行代码进行实现
newnode.next = this.head <=> node1;
this.head = newnode;
而这两行核心代码不能颠倒顺序,否则,会导致死循环。
下图为辅助理解,应当注意的是,整个过程全部都是由地址实现转换的,另外,也要把整个链表为空的情况考虑进去。
//尾插法 ✓
public void addLast(int data){
Node newnode = new Node(data);
if(this.head == null){
this.head = newnode;
return;
}
Node cur = this.head;
while(cur.next != null){
cur = cur.next;
}
cur.next = newnode;
}
如果链表中有元素,那么就遍历链表找到最后一个节点存的地址,也就是 cur.next 必定为 null,然后就把新的节点地址赋给 cur.next。
下图为辅助理解,应当注意的是,整个过程全部都是由地址实现转换的,另外,也要把整个链表为空的情况考虑进去。
尾插法相比于头插法,较为简单,但是实现的思想相同。
//寻找要插入的位置,返回插入节点的前驱 ✓
public Node searchPos(int pos){
int sign = 0;
Node cur = this.head;
while(sign + 1 != pos){
sign++;
cur = cur.next;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标 ✓
public void insertData(int pos,int data){
//1. 位置不合法
if(pos<0 || pos>size()){
System.out.println("链表的插入位置不合法!");
}
//2. 头插
else if(pos == 0){
addFirst(data);
}
//3. 尾插
else if(pos == size()){
addLast(data);
}
//4. 中间插
else{
Node newnode = new Node(data);
Node cur = searchPos(pos);
newnode.next = cur.next;
cur.next = newnode;
}
}
在中间位置插入数据之前,每个节点的 “内存” 是连着的,所以我们考虑插入数据之后,也要满足上面的要求,就像一根线把零散的珠子串起来一样,上述代码分为几种情况,读者可自行体会,而我们下面只分析中间插入节点的情况。
思路如下:
在类中创建两个方法
一个函数是 searchPos,用来找到要插入节点位置下标的,并返回节点的前驱 cur,因为前驱即将存的是我们插入节点的地址,我们假设把第一个节点的下标设置成 0,第二个节点的下标设置成 1…
另一个函数是 insertData,用来执行插入节点后的地址转换
举个例子,我们在链表中插入的位置是下标 2,那么我们通过下面的图和代码进行演示,
而这两行核心代码不能颠倒顺序,否则,会导致死循环。
newcode.next = cur.next;
cur.next = newcode;
//查找是否包含关键字key是否在单链表当中 ✓
public boolean contains(int key){
Node cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}else{
cur = cur.next;
}
}
return false;
}
//删除第一次出现关键字为key的节点 ✓
public void remove(int key){
//1. 链表为空
if(this.head == null){
System.out.println("链表为空,无法删除!");
return;
}
//2. 头删
if(this.head.val == key){
this.head = this.head.next;
return;
}
//3. 中间删 + 尾删
Node prev = this.head;
Node del = this.head;
while(prev.next != null){
if(prev.next.val != key){
prev = prev.next;
}
else{
del = prev;
del.next = del.next.next;
return;
}
}
System.out.println("你所删除的节点在链表中没有找到!");
}
这里就不再画图描述,思想和任意位置插入是一样的,所以我主要阐明一下步骤:
删除节点有如下几种情况:
(1)删除的链表是否为空
(2)如果链表不为空,删除的节点是否有对应的元素
(3)头删
(4)尾删
(5)中间删
假设我们删除的是 key 为 34,也就是 2 号位,那么我们一定要让 cur 走到 1号位,因为 1 号位是 2 号位的前驱,即1 号位存放着 2 号位的地址,然后通过如下代码,就可以完成目的,而删除尾部的节点,和此时的思想是一样的。
cur.next = cur.next.next
//删除所有出现 key 的节点 ✓
public void removeAllKey(int key){
//1. 链表为空
if(this.head == null){
System.out.println("链表为空,无法删除!");
return;
}
//2. 开始删除重复的节点
Node cur = this.head;
Node prev = this.head;
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}
else{
prev = cur;
cur = cur.next;
}
}
//3. 考虑第一个节点
if(this.head.val == key){
this.head = this.head.next;
}
}
方法一:我们可以不断调用 remove 函数,一次调用删一个值,遍历很多次,然而这样的效率太低,所以我们可以尝试方法二。
方法二:我们可以只经过一次遍历,就可以达到所有删除的目的,下面我主要讲解方法二,方法二有两种情况。
(情况1)指定的节点在链表中非连续存放
举个例子:假设我们要删除的节点对应值 key 为 23
那么下图开始我的分析
拆解步骤:
步骤①:我们创建两个引用(指针)
一个是 cur, 代表搜寻指针,指向 head.next,目的是让 cur 遍历整个链表,遇到对应的 key,那么就停下,没遇到对应的 key,就继续跑,直到遇到 null,就跳出循环。
另一个是 prev, 代表 cur 的前驱指针,指向 head,目的是跟随指针 cur,起到串联链表中零散节点的作用。
后面的步骤开始执行:
例如步骤②,如果遇到了 key ,就执行如下代码
prev.next = cur.next; //串联零散节点
cur = cur.next; //继续遍历
例如步骤③,如果没遇到 key ,就执行如下代码
prev = cur; //重置前驱
cur = cur.next; //继续遍历
最后判断第一个节点 head.val 是否 == key,是 key 就重新指向,读者可以思考,如果 head 放在程序最开始判断,那么 head 将不断被改变,因为我们要删除 key 对应的节点的位置是不可预知的,这样考虑下来,直接将头指针放置在程序最后判断较为方便。
而最后一个节点是否有对应的 key 我们不用关心,因为它仍然符合以上代码。
此外,还有一种情况:
(情况2)指定的节点在链表中连续存放
此情况被被情况(1)包含,所以仍然符合以上代码,读者可以自行画图分析。
//清空链表 ✓
public void clear(){
this.head = null;
}
public class Test {
public static void main(String[] args) {
LinkedList test = new LinkedList();
test.initialList();
test.print();
}
}
public class Test {
public static void main(String[] args) {
LinkedList test = new LinkedList();
test.addFirst(1);
test.addFirst(3);
test.addFirst(5);
test.addFirst(7);
test.print();
test.addLast(2);
test.addLast(4);
test.addLast(6);
test.addLast(8);
test.print();
test.insertData(8,77);
test.print();
test.insertData(0,99);
test.print();
test.insertData(2,88);
test.print();
}
}
public class Test {
public static void main(String[] args) {
LinkedList test = new LinkedList();
test.addLast(1);
test.addLast(3);
test.addLast(5);
test.addLast(7);
test.addLast(9);
test.print();
test.remove(9);
test.print();
test.remove(1);
test.print();
test.remove(5);
test.print();
test.remove(10);
}
}
public class Test {
public static void main(String[] args) {
LinkedList test = new LinkedList();
test.addLast(2);
test.addLast(3);
test.addLast(5);
test.addLast(2);
test.addLast(2);
test.addLast(9);
test.addLast(2);
test.print();
test.removeAllKey(2);
test.print();
}
}