Javase学习13-链表
1. 单向链表
1.1 单向链表的结构
单向链表中的节点由两部分组成:
- 节点储存的数据 data
- 指向下一个节点的地址 next
节点类:
public class Node {
//为了不让外部类使用Node类,使用private修饰data和next
/**
* 节点储存的数据
*/
private Object data;
/**
* 节点存储的指向下一个节点的地址,默认为null
*/
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
1.2 单向链表的特点
1.2.1 单向链表的优点:
相比于数组,链表的增删操作不会影响过多数据的位置,易于进行增删操作
1.2.2 单向链表的缺点:
链表的各个节点在内存中的位置是无序的,每次进行查找时只能从头节点开始依次搜寻,检索效率慢
1.3 单向链表节点的搜索
在对链表进行增删时,必须要先找到目标节点,才能进行下一步的操作
1.3.1 通过值找到目标节点
/**
* 3.1找到目标节点
*/
public Node findNode(Object value) {
Node tempNode = head;
while (tempNode != null) {
//如果临时节点的值等于目标值,那么就返回该节点
if (tempNode.equals(value) {
return tempNode;
}
tempNode = tempNode.getNext();
}
//如果没有找到,返回null
return null;
}
1.3.2 通过下标找到目标节点
/**
* 3.2通过下标找到目标节点
*/
public Node getNode(int index) {
Node tempNode = head;
for (int i = 0; i < index; i++) {
tempNode = tempNode.getNext();
}
return tempNode;
}
通过递归找到尾节点:
/**
* 找到尾节点
*/
public Node findEnd(Node node) {
if(node.getNext() == null) {
return node;
}
//通过递归调用实现遍历
return findEnd(node.getNext());
}
1.4 单向链表节点的插入
1.4.1 在指定位置插入新节点
插入新节点时,先将新节点指向目标位置的原节点,再将要插入位置的前一个节点指向新节点
public void add(int index,Object data){
//检查是下标是否越界
rangeCheck(index);
Node newNode = new Node(data,null);
//先将新节点指向目标位置的原节点
newNode.setNext(getNode(index));
//再将目标节点的前一个节点指向新节点
getNode(index - 1).setNext(newNode);
size++;
}
1.4.2 在链表头部插入新节点
这种方法是在链表前面添加节点,使每次添加的新节点成为新的头节点
public void addHead(Object data) {
//创建新节点
Node newNode = new Node(data,null);
//当链表不为空时,使新节点指向旧的头节点
if (head != null) {
newNode.setNext(head);
}
//使新节点成为新的头节点
head = newNode;
//链表长度加1
size++;
}
1.4.3 在链表尾部插入新节点
这种方法是在链表后面添加节点,使每次添加的新节点成为新的尾节点,该方法需要找到当前链表的尾节点
public void addEnd(Object data) {
//创建新节点
Node newNode = new Node(data,null);
if (head == null) {
//当链表为空时,使新节点成为头节点
head = newNode;
} else {
//当链表不为空时,找到当前链表的末节点,使其指向新节点,新节点成为新的末节点
getEnd(head).setNext(newNode);
}
//链表长度加1
size++;
}
1.5 单向链表节点的删除
删除节点时,仅需要把目标节点的前一个节点指向目标节点后的一个节点即可,未被指向的节点会被自动处理.
代码实现:
public void delete(int index) {
//判断链表中是否有数据
if (size > 0) {
//检查下标是否越界
rangeCheck(index);
//将目标节点赋给一个临时节点
Node tempNode = getNode(index);
//当链表中只有一个节点时
if (size == 1) {
head = null;
} else {
//当目标节点是头节点时
if (tempNode == head) {
head = tempNode.getNext();
}
//使目标节点的前一个节点指向目标节点的后一个结点
getNode(index - 1).setNext(tempNode.getNext());
}
//链表长度减一
size--;
}
}
1.6 实现一个可进行增删查改的单向链表
Node节点类:
package com.tsccg.practice;
/**
* @author: TSCCG
* @date: 2021/5/29
*/
public class Node {
//为了不让外部类使用Node类,使用private修饰data和next
/**
* 节点储存的数据
*/
private Object data;
/**
* 节点存储的指向下一个节点的地址,默认为null
*/
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
主类:
package com.tsccg.practice;
/**
* @author: TSCCG
* @date: 2021/5/29
*/
public class LinkedListDemo01 {
//定义头节点
private Node head;
//定义链表长度
private int size = 0;
/**
* 1.1从链表头部添加节点
*/
public void addHead(Object data) {
//创建新节点
Node newNode = new Node(data,null);
//当链表不为空时,使新节点指向旧的头节点
if (head != null) {
newNode.setNext(head);
}
//使新节点成为新的头节点
head = newNode;
//链表长度加1
size++;
}
/**
* 1.2从链表尾部添加节点
*/
public void addEnd(Object data) {
//创建新节点
Node newNode = new Node(data,null);
if (head == null) {
//当链表为空时,使新节点成为头节点
head = newNode;
} else {
//当链表不为空时,找到当前链表的末节点,使其指向新节点,新节点成为新的末节点
getEnd(head).setNext(newNode);
}
//链表长度加1
size++;
}
/**
* 1.3在指定位置插入新节点,
*/
public void add(int index,Object data){
rangeCheck(index);
Node newNode = new Node(data,null);
//先将新节点指向目标位置的原节点
newNode.setNext(getNode(index));
//再将目标节点的前一个节点指向新节点
getNode(index - 1).setNext(newNode);
size++;
}
/**
* 2.通过下标删除节点
*/
public void delete(int index) {
//判断链表中是否有数据
if (size > 0) {
//检查下标是否越界
rangeCheck(index);
//将目标节点赋给一个临时节点
Node tempNode = getNode(index);
//当链表中只有一个节点时
if (size == 1) {
head = null;
} else {
//当目标节点是头节点时
if (tempNode == head) {
head = tempNode.getNext();
}
//使目标节点的前一个节点指向目标节点的后一个结点
getNode(index - 1).setNext(tempNode.getNext());
}
//链表长度减一
size--;
}
}
/**
* 3.1通过值找到目标节点
*/
public Node getNode(Object value) {
Node tempNode = head;
while (tempNode != null) {
if (tempNode.equals(value)) {
return tempNode;
}
tempNode = tempNode.getNext();
}
//如果没有找到,返回null
return null;
}
/**
* 3.2通过下标找到目标节点
*/
public Node getNode(int index) {
rangeCheck(index);
Node tempNode = head;
for (int i = 0; i < index; i++) {
tempNode = tempNode.getNext();
}
return tempNode;
}
/**
* 3.3找到尾节点
*/
public Node getEnd(Node node) {
if (node.getNext() == null) {
return node;
}
//使用递归
return getEnd(node.getNext());
}
/**
* 4.更改目标节点的值
*/
public void update(int index, Object newData) {
//首先根据值找到目标节点,然后将新的值赋给它
getNode(index).setData(newData);
}
/**
* 5.size返回链表长度
*/
public int size() {
return size;
}
//6.isEmpty
public boolean isEmpty() {
return size() == 0;
}
/**
* 7.显示所有节点信息
*/
public void print() {
Node tempNode = head;
while (tempNode != null) {
System.out.println(tempNode.getData());
tempNode = tempNode.getNext();
}
}
//8.下标越界抛出异常
private void rangeCheck(int index) {
if (index < 0 || index > this.size()) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+this.size();
}
}
测试类:
package com.tsccg.practice;
/**
* @author: TSCCG
* @date: 2021/5/29
*/
public class LinkedListTest01 {
public static void main(String[] args) {
LinkedListDemo01 link = new LinkedListDemo01();
System.out.println("此时链表是否为空? " + link.isEmpty());
link.addEnd("AA");
link.addEnd("bb");
link.addEnd("CC");
link.addEnd("DD");
System.out.println("---------添加后---------");
link.print();
System.out.println("此时链表是否为空? " + link.isEmpty());
link.add(1,"FFF");
System.out.println("---------插入后---------");
link.print();
link.update(2,"BB");
System.out.println("---------变更后---------");
link.print();
link.delete(1);
System.out.println("---------删除后---------");
link.print();
}
}
结果:
此时链表是否为空? true
---------添加后---------
AA
bb
CC
DD
此时链表是否为空? false
---------插入后---------
AA
FFF
bb
CC
DD
---------变更后---------
AA
FFF
BB
CC
DD
---------删除后---------
AA
BB
CC
DD
2. 双向链表
2.1 双向链表的结构
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向直接后继(next)和直接前驱(pre)。
双向链表的节点由三部分组成:
- 指向前一个节点的地址 pre
- 储存的数据 data
- 指向后一个节点的地址 next
节点类:
/**
* @Author TSCCG
* @Date 2021/5/30 9:30
*/
public class Node2<T>{
//指向前一个节点的地址 pre
private Node2<T> pre;
//节点储存的数据 data
private T data;
//指向下一个节点的地址
private Node2<T> next;
//构造方法
public Node2(T data) {
this.data = data;
}
public Node2<T> getPre() {
return pre;
}
public void setPre(Node2<T> pre) {
this.pre = pre;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node2<T> getNext() {
return next;
}
public void setNext(Node2<T> next) {
this.next = next;
}
}
2.2 双向链表的特点
有两个指针,一个指向前一个节点,一个后一个节点。
2.2.1 双向链表的优点:
双向链表的每个数据结点中都有两个指针,分别指向直接后继(next)和直接前驱(pre)。故从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。
2.2.2 双向链表的缺点:
增加删除节点复杂,需要多分配一个指针存储空间。
2.3 检查数据是否越界
在进行各种操作的时候,需要检查检索的下标或输入的数据是否越界
2.3.1 检查下标是否越界
//下标越界抛出异常
private void indexCheck(int index) {
if (index < 0 || index >= this.size()) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+this.size();
}
2.3.2 检查数据是否为null
//数据为null抛出异常
private void dataCheck(T data) {
if (data == null) {
throw new NullPointerException();
}
}
2.4 双向链表的查找
在进行增删改操作时,需要先找到目标节点。
2.4.1 通过下标找到目标节点
private Node2<T> getNode(int index) {
indexCheck(index);
Node2<T> tempNode = head;
for (int i = 0; i < index; i++) {
tempNode = tempNode.getNext();
}
return tempNode;
}
2.4.2 通过值找到目标节点
private Node2<T> getNode(T data) {
dataCheck(data);
Node2<T> tempNode = head;
while (tempNode != null) {
if (tempNode.getData() == data) {
return tempNode;
}
tempNode = tempNode.getNext();
}
//如果没有找到,返回null
return null;
}
2.4.3 找到尾节点
private Node2<T> getEnd(Node2<T> node) {
if (node.getNext() == null) {
return node;
}
//使用递归
return getEnd(node.getNext());
}
2.4.4 查询目标节点的值
public T get(int index) {
indexCheck(index);
return getNode(index).getData();
}
2.5 双向链表的插入
在指定位置插入节点,过程如下:
- 首先,我们需要创建一个新节点newNode,然后找到目标位置的节点getNode(index),将该节点赋给一个临时节点tempNode.
- 先将新节点的后继next指向目标位置节点
- 再将目标位置节点的前一个节点的后继next指向新节点
- 然后将新节点的前驱pre指向目标位置节点的前一个节点
- 最后将目标位置节点的前驱pre指向新节点
/**
* 在指定位置插入新节点
*/
public void add(int index,T data){
indexCheck(index);
//创建新节点
Node2<T> newNode = new Node2<>(data);
//将目标位置的节点赋给一个临时节点
Node2<T> tempNode = getNode(index);
//当要在头节点前插入时
if (index == 0) {
newNode.setNext(tempNode);
tempNode.setPre(newNode);
head = newNode;
} else {
//先将新节点的后继next指向目标位置节点
newNode.setNext(tempNode);
//再将目标位置节点的前一个节点的后继next指向新节点
tempNode.getPre().setNext(newNode);
//然后将新节点的前驱pre指向目标位置节点的前一个节点
newNode.setPre(tempNode.getPre());
//最后将目标位置节点的前驱pre指向新节点
tempNode.setPre(newNode);
}
size++;
}
2.6 双向链表的添加
链表的添加一般向链表末尾添
添加过程:
- 当链表为空时,使新添加的节点成为头节点
- 当链表不为空时,找到当前链表的末节点,将末节点赋给一个临时节点
- 将末节点的后继next指向新节点
- 再将新节点的前驱pre指向末节点
- 如此,新节点就成为了新的末节点
/**
* 1.从链表尾部添加节点
*/
public void add(T data) {
//创建新节点
Node2<T> newNode = new Node2<>(data);
if (head == null) {
//当链表为空时,使新节点成为头节点
head = newNode;
} else {
//当链表不为空时,找到当前链表的末节点,将末节点赋给一个临时节点
Node2<T> tempNode = getEnd(head);
//使末节点的后继next指向新节点
tempNode.setNext(newNode);
//将新节点的前驱pre指向旧的末节点,新节点成为新的末节点
newNode.setPre(tempNode);
}
//链表长度加1
size++;
}
2.7 双向链表的删除
/**
* 2.通过下标删除节点
*/
public void delete(int index) {
//判断链表中是否有数据
if (size > 0) {
//检查下标是否越界
indexCheck(index);
//将目标节点赋给一个临时节点
Node2<T> tempNode = getNode(index);
//当链表中只有一个节点时
if (size == 1) {
head = null;
} else {
//当目标节点是头节点时
if (tempNode == head) {
//将头节点的下一个节点的前驱pre指向null
tempNode.getNext().setPre(null);
//使头节点的下一个节点成为新的头节点
head = tempNode.getNext();
} else {
//使目标节点的前一个节点的后继next指向目标节点的后一个结点
tempNode.getPre().setNext(tempNode.getNext());
//使目标节点的后一个节点的前驱pre指向目标节点的前一个节点
tempNode.getNext().setPre(tempNode.getPre());
}
}
//链表长度减一
size--;
}
}
2.8 实现一个可进行增删查改的双向链表
节点类:
/**
* @Author TSCCG
* @Date 2021/5/30 9:30
*/
public class Node2<T>{
private Node2<T> pre;
private T data;
private Node2<T> next;
public Node2(T data) {
this.data = data;
}
public Node2<T> getPre() {
return pre;
}
public void setPre(Node2<T> pre) {
this.pre = pre;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node2<T> getNext() {
return next;
}
public void setNext(Node2<T> next) {
this.next = next;
}
}
主类:
/**
* @Author TSCCG
* @Date 2021/5/30 9:28
*/
public class LinkedList2<T> {
//定义头节点
private Node2<T> head;
//定义链表长度
private int size = 0;
/**
* 1.从链表尾部添加节点
*/
public void add(T data) {
//创建新节点
Node2<T> newNode = new Node2<>(data);
if (head == null) {
//当链表为空时,使新节点成为头节点
head = newNode;
} else {
//当链表不为空时,找到当前链表的末节点,将末节点赋给一个临时节点
Node2<T> tempNode = getEnd(head);
//使末节点的后继next指向新节点,新节点成为新的末节点
tempNode.setNext(newNode);
//将新节点的前驱pre指向旧的末节点
newNode.setPre(tempNode);
}
//链表长度加1
size++;
}
/**
* 1.2在指定位置插入新节点
*/
public void add(int index,T data){
indexCheck(index);
//定义新节点
Node2<T> newNode = new Node2<>(data);
//将目标位置的节点赋给一个临时节点
Node2<T> tempNode = getNode(index);
//当要在头节点前插入时
if (index == 0) {
newNode.setNext(tempNode);
tempNode.setPre(newNode);
head = newNode;
} else {
//先将新节点的后继next指向目标位置节点
newNode.setNext(tempNode);
//再将目标位置节点的前一个节点的后继next指向新节点
tempNode.getPre().setNext(newNode);
//然后将新节点的前驱pre指向目标位置节点的前一个节点
newNode.setPre(tempNode.getPre());
//最后将目标位置节点的前驱pre指向新节点
tempNode.setPre(newNode);
}
size++;
}
/**
* 2.通过下标删除节点
*/
public void delete(int index) {
//判断链表中是否有数据
if (size > 0) {
//检查下标是否越界
indexCheck(index);
//将目标节点赋给一个临时节点
Node2<T> tempNode = getNode(index);
//当链表中只有一个节点时
if (size == 1) {
head = null;
} else {
//当目标节点是头节点时
if (tempNode == head) {
//将头节点的下一个节点的前驱pre指向null
tempNode.getNext().setPre(null);
//使头节点的下一个节点成为新的头节点
head = tempNode.getNext();
} else {
//使目标节点的前一个节点的后继next指向目标节点的后一个结点
tempNode.getPre().setNext(tempNode.getNext());
//使目标节点的后一个节点的前驱pre指向目标节点的前一个节点
tempNode.getNext().setPre(tempNode.getPre());
}
}
//链表长度减一
size--;
}
}
/**
* 3.1通过值找到目标节点
*/
private Node2<T> getNode(T data) {
dataCheck(data);
Node2<T> tempNode = head;
while (tempNode != null) {
if (tempNode.getData() == data) {
return tempNode;
}
tempNode = tempNode.getNext();
}
//如果没有找到,返回null
return null;
}
/**
* 3.2通过下标找到目标节点
*/
private Node2<T> getNode(int index) {
indexCheck(index);
Node2<T> tempNode = head;
for (int i = 0; i < index; i++) {
tempNode = tempNode.getNext();
}
return tempNode;
}
/**
* 3.3找到尾节点
*/
private Node2<T> getEnd(Node2<T> node) {
if (node.getNext() == null) {
return node;
}
//使用递归
return getEnd(node.getNext());
}
/**
* 3.4查询目标节点的值
*/
public T get(int index) {
indexCheck(index);
return getNode(index).getData();
}
/**
* 4.1使用下标更改目标节点的值
*/
public void update(int index, T newData) {
//首先根据值找到目标节点,然后将新的值赋给它
getNode(index).setData(newData);
}
/**
* 4.2 通过值更改目标节点的值
*/
public void update(T oldData, T newData) {
//检查值是否为空
dataCheck(newData);
//首先根据值找到目标节点,然后将新的值赋给它
getNode(oldData).setData(newData);
}
/**
* 5.size
*/
public int size() {
return size;
}
//6.isEmpty
public boolean isEmpty() {
return size() == 0;
}
/**
* 7.显示所有节点信息
*/
public void print() {
Node2<T> tempNode = head;
while (tempNode != null) {
System.out.println(tempNode.getData());
tempNode = tempNode.getNext();
}
}
//8.下标越界抛出异常
private void indexCheck(int index) {
if (index < 0 || index >= this.size()) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+this.size();
}
//8.2检查值是否为null
private void dataCheck(T data) {
if (data == null) {
throw new NullPointerException();
}
}
}
测试类:
/**
* @Author TSCCG
* @Date 2021/5/30 10:02
*/
public class LinkedListTest {
public static void main(String[] args) {
LinkedList2<Object> link = new LinkedList2<>();
System.out.println("此时链表是否为空? " + link.isEmpty());
link.add("张三");
link.add("李四");
link.add("王五");
link.add("赵六");
System.out.println("---------添加数据后---------");
link.print();
System.out.println("此时链表是否为空? " + link.isEmpty());
link.add(1,"阿珍");
System.out.println("---------在下标为1的位置插入阿珍后---------");
link.print();
link.update(2,"阿强");
System.out.println("---------将下标为2的节点数据更改为阿强后---------");
link.print();
link.delete(0);
System.out.println("---------删除下标为0的节点后---------");
link.print();
System.out.println("此时链表中元素的个数为:" + link.size());
System.out.println("下标为0的数据是:" + link.get(0));
}
}
结果:
此时链表是否为空? true
---------添加数据后---------
张三
李四
王五
赵六
此时链表是否为空? false
---------在下标为1的位置插入阿珍后---------
张三
阿珍
李四
王五
赵六
---------将下标为2的节点数据更改为阿强后---------
张三
阿珍
阿强
王五
赵六
---------删除下标为0的节点后---------
阿珍
阿强
王五
赵六
此时链表中元素的个数为:4
下标为0的数据是:阿珍
3. 总结
3.1 单向链表和双向链表的区别:
3.1.1 单向链表:
只有一个指向下一个节点的指针。
优点:单向链表增加删除节点简单。遍历时候不会死循环;
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。
适用于节点的增加删除。
3.1.2 双向链表:
有两个指针,一个指向前一个节点,一个后一个节点。
优点:可以找到前驱和后继,可进可退;
缺点:增加删除节点复杂,需要多分配一个指针存储空间。
System.out.println("此时链表是否为空? " + link.isEmpty());
link.add(1,"阿珍");
System.out.println("---------在下标为1的位置插入阿珍后---------");
link.print();
link.update(2,"阿强");
System.out.println("---------将下标为2的节点数据更改为阿强后---------");
link.print();
link.delete(0);
System.out.println("---------删除下标为0的节点后---------");
link.print();
System.out.println("此时链表中元素的个数为:" + link.size());
System.out.println("下标为0的数据是:" + link.get(0));
}
}
#### 结果:
```java
此时链表是否为空? true
---------添加数据后---------
张三
李四
王五
赵六
此时链表是否为空? false
---------在下标为1的位置插入阿珍后---------
张三
阿珍
李四
王五
赵六
---------将下标为2的节点数据更改为阿强后---------
张三
阿珍
阿强
王五
赵六
---------删除下标为0的节点后---------
阿珍
阿强
王五
赵六
此时链表中元素的个数为:4
下标为0的数据是:阿珍
3. 总结
3.1 单向链表和双向链表的区别:
3.1.1 单向链表:
只有一个指向下一个节点的指针。
优点:单向链表增加删除节点简单。遍历时候不会死循环;
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。
适用于节点的增加删除。
3.1.2 双向链表:
有两个指针,一个指向前一个节点,一个后一个节点。
优点:可以找到前驱和后继,可进可退;
缺点:增加删除节点复杂,需要多分配一个指针存储空间。
适用于需要双向查找节点值的情况。