1.链表是什么?
1.1 链表的概念与结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
其中 Data 用来存储数据元素,而 Next用来存储后一个Data的地址,最后一个结点的指针域为空。
这就是单链表,当然链表不止是只有这一种形式,还有双链表:
双链表的特点就是不仅知道后一个节点的位置,还知道前一个节点的位置,这篇文章就是要模拟这两种链表。
2.模拟实现单链表
2.1 开始模拟
(1)单链表是一个一个节点连接而成的,我们用什么来表示这一个一个节点呢?答案是内部类:
public class MySingleList {
private static class Node{
public int val;//存储的元素
public Node next;//下一个节点
//构造方法
public Node(int val){
this.val = val;
}
}
}
(2)由于单链表的物理存储位置是松散的,不像数组那样自带开始位置的标识(也就是 数组名[0]),这里的单链表需要一个变量来代表开始位置的节点,以方便我们找到它。(这里没有实际的头节点,仅仅是用一个变量来指向第一个元素来代表头节点,初始值为null。)
public class MySingleList {
private static class Node{
public int val;//存储的元素
public Node next;//下一个节点
//构造方法
public Node(int val){
this.val = val;
}
}
//用 head 变量来指向第一个元素
private Node head;
}
(3)实现打印单链表的方法,要实现这个方法必须要知道如何遍历单链表:
//打印单链表
public void print(){
Node cur = this.head;//从第一个元素开始
//遍历单链表,当cur为null时就表示链表遍历完了。
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;//将cur移到下一个元素,这里的 cur.next 就表示 cur 的后一个元素。
}
System.out.println();//换行
}
(4)头插的实现:是什么头插?就是插入元素的时候,把它插到最前面,这时它就是第一个节点了。
//头插
public void addFirst(int val){
//当head为空的时候下面代码也成立
Node newNode = new Node(val);
newNode.next = head;
head = newNode;
}
(5)头插已经实现了,现在来实现尾插,要注意的是,尾插的时候我们必须要知道最后一个节点才能实现尾插,如何找到最后一个节点?答案是遍历。
//尾插法
public void addLast(int val){
Node newNode = new Node(val);//新节点
if(head == null){//当链表为空的时候
head = newNode;
}else {
Node cur = head;//cur用来当作工具
//遍历找到最后一个节点
while(cur.next != null){//要注意这里是 cur.next
cur = cur.next;
}
//找到后将 newNode 插入到最后
cur.next = newNode;
}
}
(6)在index前插入数据:我们需要知道index前一个节点才能进行插入操作,这里就写一个findIndexNode方法来查找节点。
//查找index下标的节点
public Node findIndexNode(int index){
Node cur = head;
//查找index下标的节点
while(index-- != 0){
cur = cur.next;
}
return cur;
}
//获取链表的长度
public int size(){
Node cur = head;
int size = 0;
//遍历链表
while(cur != null){
size++;
cur = cur.next;
}
return size;
}
//在index前插入指定的数据(第一个数据为 0 下标)
public void add(int index,int val) throws IndexOutOfBoundsException{
//index == size() 的时候是尾插,所以这里 “>” 就行了
if(index < 0 || index > size()){
//抛出一个异常(这个异常是java自带的,表示下标越界)
throw new IndexOutOfBoundsException("index 不合法!");
} else if (index == 0) {
addFirst(val);//头插
} else if (index == size()) {
addLast(val);//尾插
} else {
//先找到前一个节点
Node cur = findIndexNode(index - 1);
Node newNode = new Node(val);
//修改指向
newNode.next = cur.next;
cur.next = newNode;
}
}
(7)对于查找板块,我们要实现如下方法:
//查找单链表当中是否包含关键字key
public boolean contains(int key){
Node cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//查找下标为 index 位置的元素
public int get(int index){
if(index < 0 || index >= size()){
throw new IndexOutOfBoundsException("index 非法!");
}
Node cur = head;
//为什么从 1 开始?因为cur = head
for (int i = 1; i <= index; i++) {
cur = cur.next;
}
return cur.val;
}
(8)删除板块:删除第一次出现关键字为key的节点,我们得先找到key节点的前一个节点,还要记录key这个节点。
//查找关键字 key 的前一个节点
private Node findPrevOfKey(int key) {
Node cur = head;
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
//如果链表为空
if(head == null){
return;
}
//如果key是头节点
if(head.val == key){
head = head.next;//如果只有 head 这一个节点也是可以这样操作的,这时head = null。
return;
}
//找到 key 的前一个节点
Node cur = findPrevOfKey(key);
if(cur == null){
System.out.println("没有你要删除的数字!");
return;
}
//删除节点
Node tmp = cur.next;
cur.next = tmp.next;
}
(9)删除所有值为key的节点,删除逻辑与上一个类似,但是这里需要两个指针来遍历整个链表。
//删除所有值为key的节点
public void removeAllKey(int key){
if(head == null){
return;
}
//头删:当头节点是key时
while(head.val == key){
head = head.next;
}
Node cur = head.next;
Node prev = head;//指向cur的前一个
//遍历链表
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
}
2.2 汇总
public class MySingleList {
private static class Node{
public int val;//存储的元素
public Node next;//下一个节点
//构造方法
public Node(int val){
this.val = val;
}
}
//用 head 变量来指向第一个元素
private Node head;
//打印单链表
public void print(){
Node cur = this.head;//从第一个元素开始
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;//将cur移到下一个元素,这里的 cur.next 就表示 cur 的后一个元素。
}
System.out.println();//换行
}
//头插
public void addFirst(int val){
//当head为空的时候下面代码也成立
Node newNode = new Node(val);
newNode.next = head;
head = newNode;
}
//尾插法
public void addLast(int val){
Node newNode = new Node(val);//新节点
if(head == null){//当链表为空的时候
head = newNode;
}else {
Node cur = head;//cur用来当作工具
//遍历找到最后一个节点
while(cur.next != null){//要注意这里是 cur.next
cur = cur.next;
}
//找到后将 newNode 插入到最后
cur.next = newNode;
}
}
//查找index下标的节点
public Node findIndexNode(int index){
Node cur = head;
//查找index下标的节点
while(index-- != 0){
cur = cur.next;
}
return cur;
}
//获取链表的长度
public int size(){
Node cur = head;
int size = 0;
//遍历链表
while(cur != null){
size++;
cur = cur.next;
}
return size;
}
//在index前插入指定的数据(第一个数据为 0 下标)
public void add(int index,int val) throws IndexOutOfBoundsException{
//index == size() 的时候是尾插,所以这里 “>” 就行了
if(index < 0 || index > size()){
//抛出一个异常(这个异常是java自带的)
throw new IndexOutOfBoundsException("index 不合法!");
} else if (index == 0) {
addFirst(val);//头插
} else if (index == size()) {
addLast(val);//尾插
} else {
//先找到前一个节点
Node cur = findIndexNode(index - 1);
Node newNode = new Node(val);
//修改指向
newNode.next = cur.next;
cur.next = newNode;
}
}
//查找单链表当中是否包含关键字key
public boolean contains(int key){
Node cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//查找下标为 index 位置的元素
public int get(int index){
if(index < 0 || index >= size()){
throw new IndexOutOfBoundsException("index 非法!");
}
Node cur = head;
//为什么从 1 开始?因为cur = head
for (int i = 1; i <= index; i++) {
cur = cur.next;
}
return cur.val;
}
//查找关键字 key 的前一个节点
private Node findPrevOfKey(int key) {
Node cur = head;
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
//如果链表为空
if(head == null){
return;
}
//如果key是头节点
if(head.val == key){
head = head.next;//如果只有 head 这一个节点也是可以这样操作的,这时head = null。
return;
}
//找到 key 的前一个节点
Node cur = findPrevOfKey(key);
if(cur == null){
System.out.println("没有你要删除的数字!");
return;
}
//删除节点
Node tmp = cur.next;
cur.next = tmp.next;
}
//删除所有值为key的节点
public void removeAllKey(int key){
if(head == null){
return;
}
//头删:当头节点是key时
while(head.val == key){
head = head.next;
}
Node cur = head.next;
Node prev = head;//指向cur的前一个
//遍历链表
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
}
}
3.模拟实现双向链表
3.1 开始模拟
双向链表的示意图:
每个节点包含两个指针:一个指向前一个节点,一个指向后一个节点。与单向链表不同的是,双向链表可以在任意方向上遍历。因此,它可以从前往后遍历,也可以从后往前遍历。
其实双向链表的很多操作都与单向链表相同。
(1)节点的实现:
public class MyLinkedList {
private static class Node{
public int val;
public Node prev;//前一个节点
public Node next;//后一个节点
public Node(int val) {
this.val = val;
}
}
private Node head;//指向头节点,默认值为null
private Node tail;//指向尾节点,默认值为null
}
(2)打印整个双向链表、获取链表长度,对于双向链表的遍历,跟单向链表的操作是相同的。
//打印双向链表
public void print(){
if (this.head == null) {
System.out.println("null");
return;
}
Node cur = head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//获取双向链表的长度
public int size(){
Node cur = head;
int tmp = 0;
while(cur != null){
tmp++;
cur = cur.next;
}
return tmp;
}
(3)头插:
//头插
public void addFirst(int data){
Node newNode = new Node(data);
//当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
if(head == null){
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
(4)尾插:
//尾插
public void addLast(int data){
Node newNode = new Node(data);
//当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
if(head == null){
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
(5)任意位置插入:
//查找 index 位置的节点,第一个下标为0
public Node findIndexNode(int index) throws IndexOutOfBoundsException{
if(index<0 || index >= this.size()){
//这里我们用java自带的异常
throw new IndexOutOfBoundsException("index位置非法!");
}
Node cur = head;
while(index != 0){
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex (int index,int data){
if(index<0 || index > this.size()){
//这里我们用java自带的异常
throw new IndexOutOfBoundsException("index位置非法!");
}
if(index == 0){//当下标为0时,头插
addFirst(data);
return;
} else if (index == size()) {//当下标为 size() 时(最后一个数据的后一个),尾插
addLast(data);
return;
}
//找到 index 的前一个节点
Node cur = findIndexNode(index - 1);//调用了上面的方法
Node newNode = new Node(data);
newNode.prev = cur;
newNode.next = cur.next;
cur.next.prev = newNode;
cur.next = newNode;
}
(6)查找是否包含关键字key:
//查找是否包含关键字key 是否在单链表当中
public boolean contains(int key){
Node cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
(7)删除第一次出现关键字为key的节点:这个删除操作要分很多种情况。
① 当我删除的key是head的时候,这个时候又要分两种情况:1.只有一个节点。2.多个节点。
② key是尾节点tail的时候
③ key是中间节点的时候
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head == null){
return;
}
//如果删除的是头节点
if(head.val == key){
//如果只有一个节点
if(head.next == null){
head = null;
tail = null;
return;
}
//head后面还有节点的时候。
head = head.next;
head.prev = null;
}
//移动到 第一个 key
Node cur = head.next;
while(cur.val != key){
cur = cur.next;
//链表中没有key这个数据
if(cur == null){
return;
}
}
//(在while循环后,是保证第一次遇到key)如果key是最后一个节点,要最特殊处理。
if(tail == cur){
tail = tail.prev;
tail.next = null;
return;
}
//key在中间节点的情况
Node curPrev = cur.prev;
curPrev.next = cur.next;
cur.next.prev = cur.prev;
cur = null;
}
(8)删除所有值为key的节点。
//删除所有值为key的节点
public void removeAllKey(int key){
if(head == null){
return;
}
//这里为什么是用 while ? 因为我们是要删除所有的key,只要 key 是 head 节点都是需要特殊处理的。
while(head.val == key){
//当只有head节点的时候
if(head.next == null){
head = null;
tail = null;//必须也要让 tail = null
return;
}
head = head.next;
head.prev = null;//必须要做这一步,这样才能让前一个节点彻底断开
}
//普通情况
Node cur = head.next;
while(cur != null){
if(cur.val == key){
//如果key是tail的时候
if(cur.next == null){
tail = tail.prev;
tail.next = null;
return;
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
cur = cur.next;
}
}
(9)删除整个链表,注意,这不是简单的将头尾指针置为null就够了,需要将每一个节点的prev与next都要置为null;
//删除整个链表
public void clear(){
if(head == null){
return;
}
Node cur = head;
Node curNext = cur.next;
//先将头尾指针释放
tail = null;
head = null;
//遍历每一个节点,将每一个节点的 prev 和 next 置为null
while(cur != null){
cur.prev = null;
cur.next = null;
cur = curNext;
//当 curNext = null时 遍历结束
if(curNext != null){
curNext = cur.next;
}
}
}
3.2 汇总
public class MyLinkedList {
private static class Node{
public int val;
public Node prev;//前一个节点
public Node next;//后一个节点
public Node(int val) {
this.val = val;
}
}
private Node head;//指向头节点
private Node tail;//指向尾节点
//打印双向链表
public void print(){
if (this.head == null) {
System.out.println("null");
return;
}
Node cur = head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//获取双向链表的长度
public int size(){
Node cur = head;
int tmp = 0;
while(cur != null){
tmp++;
cur = cur.next;
}
return tmp;
}
//头插
public void addFirst(int data){
Node newNode = new Node(data);
//当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
if(head == null){
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
//尾插
public void addLast(int data){
Node newNode = new Node(data);
if(head == null){
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
//查找 index 位置的节点,第一个下标为0
public Node findIndexNode(int index) throws IndexOutOfBoundsException{
if(index<0 || index >= this.size()){
//这里我们用java自带的异常
throw new IndexOutOfBoundsException("index位置非法!");
}
Node cur = head;
while(index != 0){
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex (int index,int data){
if(index<0 || index > this.size()){
//这里我们用java自带的异常
throw new IndexOutOfBoundsException("index位置非法!");
}
if(index == 0){//当下标为0时,头插
addFirst(data);
return;
} else if (index == size()) {//当下标为 size() 时(最后一个数据的后一个),尾插
addLast(data);
return;
}
//找到 index 的前一个节点
Node cur = findIndexNode(index - 1);
Node newNode = new Node(data);
newNode.prev = cur;
newNode.next = cur.next;
cur.next.prev = newNode;
cur.next = newNode;
}
//查找是否包含关键字key 是否在单链表当中
public boolean contains(int key){
Node cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head == null){
return;
}
//如果删除的是头节点
if(head.val == key){
//如果只有一个节点
if(head.next == null){
head = null;
tail = null;
return;
}
//多个节点
head = head.next;
head.prev = null;
}
//移动到 第一个 key
Node cur = head.next;
while(cur.val != key){
cur = cur.next;
//链表中没有key这个数据
if(cur == null){
return;
}
}
//(在while循环后,是保证第一次遇到key)如果key是最后一个节点,要最特殊处理。
if(tail == cur){
tail = tail.prev;
tail.next = null;
return;
}
//key在中间节点的情况
Node curPrev = cur.prev;
curPrev.next = cur.next;
cur.next.prev = cur.prev;
cur = null;
}
//删除所有值为key的节点
public void removeAllKey(int key){
if(head == null){
return;
}
//这里为什么是用 while ? 因为我们是要删除所有的key,只要 key 是 head 节点都是需要特殊处理的。
while(head.val == key){
//当只有head节点的时候
if(head.next == null){
head = null;
tail = null;//必须也要让 tail = null
return;
}
head = head.next;
head.prev = null;//必须要做这一步,这样才能让前一个节点彻底断开
}
//普通情况
Node cur = head.next;
while(cur != null){
if(cur.val == key){
//如果key是tail的时候
if(cur.next == null){
tail = tail.prev;
tail.next = null;
return;
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
cur = cur.next;
}
}
//删除整个链表
public void clear(){
if(head == null){
return;
}
Node cur = head;
Node curNext = cur.next;
//先将头尾指针释放
tail = null;
head = null;
//遍历每一个节点,将每一个节点的 prev 和 next 置为null
while(cur != null){
cur.prev = null;
cur.next = null;
cur = curNext;
//当 curNext = null时 遍历结束
if(curNext != null){
curNext = cur.next;
}
}
}
}
上面只是完成了常用的方法,大家在练习的时候可以进行扩展。
4.LinkedList 类
4.1 什么是 LinkedList ?
详细解读Java中的ArrayList集合类 以及 用Java简单模拟实现顺序表 - 掘金 (juejin.cn)
Java中的LinkedList类是一种实现了List接口的双向链表数据结构。它可以在任意位置添加或删除元素。
与ArrayList不同,LinkedList的访问时间复杂度是O(n),因为需要遍历链表来查找元素,但是在添加和删除元素时的时间复杂度是O(1),只需要修改指针即可。LinkedList适用于频繁的插入和删除操作,但不适用于随机访问。
4.2 LinkedList 类的使用
4.2.1 LinkedList 类的构造方法
| 构造方法 | 描述 |
|---|---|
| LinkedList() | 创建一个空的链表 |
| LinkedList(Collection<? extends E> c) | 创建一个包含指定集合中所有元素的链表 |
其中,LinkedList()构造方法创建一个空的链表,LinkedList(Collection<? extends E> c)构造方法创建一个包含指定集合中所有元素的链表。如果需要创建一个已经包含元素的链表对象,可以使用该构造方法。
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
public class LinkedListExample {
public static void main(String[] args) {
// 创建一个ArrayList对象
List<String> arrayList = new ArrayList<>();
arrayList.add("apple");
arrayList.add("banana");
arrayList.add("orange");
// 使用LinkedList(Collection<? extends E> c)构造方法创建一个包含指定集合中所有元素的链表
LinkedList<String> linkedList = new LinkedList<>(arrayList);
// 输出链表中的元素
System.out.println(linkedList); // [apple, banana, orange]
}
}
4.2.2 LinkedList 类中常用方法
| 方法名 | 描述 |
|---|---|
| add(E e) | 在链表末尾添加元素 |
| add(int index, E element) | 在指定位置插入元素 |
| addFirst(E e) | 在链表头部添加元素 |
| addLast(E e) | 在链表末尾添加元素 |
| clear() | 删除链表中的所有元素 |
| contains(Object o) | 判断链表中是否包含指定元素 |
| get(int index) | 获取指定位置的元素 |
| getFirst() | 获取链表头部的元素 |
| getLast() | 获取链表尾部的元素 |
| indexOf(Object o) | 返回指定元素在链表中第一次出现的位置 |
| isEmpty() | 判断链表是否为空 |
| iterator() | 返回链表的迭代器 |
| remove(Object o) | 删除链表中第一次出现的指定元素 |
| remove(int index) | 删除指定位置的元素 |
| removeFirst() | 删除链表头部的元素 |
| removeLast() | 删除链表尾部的元素 |
| size() | 返回链表中元素的个数 |
| toArray() | 将链表转换为数组 |
4.2.3 ArrayList 和 LinkedList 的区别
| 区别 | ArrayList | LinkedList |
|---|---|---|
| 内部实现 | 基于动态数组 | 基于双向链表 |
| 随机访问 | 时间复杂度为O(1) | 时间复杂度为O(n) |
| 插入和删除操作 | 时间复杂度为O(n) | 时间复杂度为O(1) |
| 内存空间 | 内存空间连续 | 内存空间不连续 |
| 迭代器 | 支持快速随机访问 | 不支持快速随机访问 |
| 适用场景 | 随机访问频繁,插入和删除操作较少 | 插入和删除操作频繁,随机访问较少 |