一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 23 天,点击查看活动详情
日积月累,水滴石穿 😄
链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点(链表中每一个元素称为节点)组成,每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
来源于百度百科
链表类别
- 单链表
- 循环单链表
- 双链表
- 循环双链表
链表优点
- 链表插入和删除速度快,只需要修改相邻节点的指针域的指向。
- 可以充分利用计算机内存空间,不必预先知道数据大小。
链表缺点
- 存储数据同时还需要额外存储下一个节点的地址(单链表,双链表还要维护上一个节点地址),空间开销比较大。
- 不能像数组那样直接找到指定节点,需要从链表的头节点或尾节点进行遍历,查询速度较慢。
单链表
链表中的每个节点都有一个指针指向下一个节点,最后一个节点的 next 指向 NULL。这个就是单链表。
- item:用来存放元素
- next:用来指向下一个节点元素 从图中可以发现,有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们把第一个结点叫作头结点,把最后一个结点叫作尾结点。 其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。
伪代码结构
private class Node<E> {
E item;
Node<E> next;
}
循环单链表
循环单链表其实就是单链表,区别在于最后一个节点的 next 指向头节点,整个链表形成一个环。
双链表
双链表是对单链表的改进,双链表与单链表相比,多了一个存储上一个结点地址的指针域,名为 prev。双向链表需要额外的一个空间来存储前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然多了一个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
看图可以知道,双向链表是包含两个指针的,
prev 指向前一个节点,next指向后一个节点,但是第一个节点也就是头节点的prev指向 null,最后一个节点也就是尾节点指向 null。
伪代码结构
private class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
循环双链表
第一个节点也就是头节点
prev指向尾节点,最后一个节点也就是尾节点 next指向头结点,形成环。
自定义单链表
上面讲了这么多,开始我们的实战,直接上代码。
public class MyLinkedList {
private MyNode node;
int size = 0;
}
class MyNode{
String item; //值
MyNode next; //下一个节点的指针
MyNode(String item){
this.value = value;
this.next = null;
}
}
- MyNode:这个就是节点。节点中有值、有下一个节点的指针。
- size:记录链表节点的个数。
addHead
添加节点到链表头部。
/**
* 插入链表头部
*/
public void addHead(String data){
MyNode newNode = new MyNode(data);
// head = myNode; 如果head 为 null,可以这么写
// 但如果 head有值了呢?再插入一个值到头部,原有的值就会被覆盖
newNode.next = head;
head = newNode;
size++;
}
add
/**
* 添加节点到链表尾部
*/
public void add(String data){
MyNode newNode = new MyNode(data);
MyNode cur = head;
//遍历到末尾元素 末尾元素 next 指向 null
while (cur.next != null){
cur = cur.next;
}
//直接将末尾的 next 指向新元素
cur.next = newNode;
size++;
}
获得末尾节点,将末尾节点的 next 指向新节点。
add(int index,String data)
指定下标添加节点
/**
* 指定下标添加链表节点
*/
public void add(int index, String data) {
//下标越界
if(index > size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
//判断是否添加的是头结点
if(index == 0) {
addHead(data);
}
else {
// 如果不是头节点,则需要进行遍历,获得下标前一个节点
//比如源链表 1、2、3、4 ,在下标为 2 插入节点 5
//目标链表为 1、2、5、3、4
//1、因为单链表只有 next 指针,所以要找到指定下标节点的前一个节点
//2、如果 index 不减 1,当需要在尾部添加节点时,会出现空指针
MyNode cur = head;
for(int i = 0; i < index - 1; i++) {
cur = cur.next;
}
MyNode newNode = new MyNode(data);
//将下标节点与新节点关联起来
newNode.next = cur.next;
//next 指向新节点
cur.next = newNode;
size++;
}
}
指定下标添加节点方法,想对于添加末尾方法,可以说就多了一行新代码:newNode.next = cur.next;,需要维护当前插入节点的下个节点是什么,而插入到尾部则不需维护。
remove
/**
* 根据下标删除节点
*/
public void remove(int index) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
MyNode cur = head;
//如果删除头节点
if(index == 0) {
removeHead();
return;
}
//找到指定下标节点的前一个节点
for(int i = 0; i < index - 1; i++) {
cur = cur.next;
}
//跳过要删除元素的指向
cur.next = cur.next.next;
size--;
}
removeHead
/**
* 删除链表头节点
*/
private void removeHead() {
if(head == null) {
return;
}
//获得头节点的下个节点
MyNode headNext = head.next;
//将下个节点赋值给头节点
head = headNext;
size--;
}
removeTail
/**
* 删除链表尾节点
*/
public void removeTail() {
remove(size - 1);
}
set
/**
* 修改指定下标节点值
*/
public void set(int index, String data) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
//获得指定下标节点
MyNode node = getNode(index);
node.item = data;
}
get
/**
* 获得指定下标节点的值
*/
public String get(int index) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
//获得指定下标节点
MyNode node = getNode(index);
return node.item;
}
public void print() {
MyNode cur = head;
while(cur != null) {
System.out.println(cur.item + " ");
cur = cur.next;
}
}
getNode
获得指定下标节点
/**
* 获得指定下标节点
*/
private MyNode getNode(int index) {
MyNode cur = head;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
测试数据
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
System.out.println("添加头节点");
myLinkedList.addHead("2");
myLinkedList.addHead("1");
myLinkedList.print();
System.out.println("在尾部添加节点");
myLinkedList.add("3");
myLinkedList.add("4");
myLinkedList.print();
System.out.println("指定下标添加节点");
myLinkedList.add(0, "666");
myLinkedList.add(5, "777");
myLinkedList.add(2, "888");
myLinkedList.print();
System.out.println("指定下标删除节点");
myLinkedList.remove(0);
myLinkedList.remove(5);
myLinkedList.removeHead();
myLinkedList.removeTail();
myLinkedList.print();
System.out.println("指定下标修改节点");
myLinkedList.set(0, "cxyxj");
myLinkedList.print();
System.out.println("指定下标获得节点");
System.out.println(myLinkedList.get(0));
}
各位小伙伴,图我就没划了,太麻烦了,也太耗时间了,可能我也画不出来。各位自己下去用 1 -> 2 -> 3 -> 4 -> 5替代吧。
自定义双链表
/**
* @author cxyxj
*/
public class MyDoubleLinkedList {
/**
* 头节点
*/
MyDoubleNode headNode;
/**
* 尾节点
*/
MyDoubleNode tailNode;
/**
* 节点的个数
*/
int size;
private static class MyDoubleNode {
/**
* 值
*/
String item;
/**
* 下一个节点的指针
*/
MyDoubleNode next;
/**
* 上一个节点的指针
*/
MyDoubleNode prev;
MyDoubleNode(MyDoubleNode prev, String data, MyDoubleNode next) {
this.item = data;
this.next = next;
this.prev = prev;
}
}
public void print() {
MyDoubleNode cur = headNode;
while(cur != null) {
System.out.println(cur.item + " ");
cur = cur.next;
}
}
}
addHead
添加节点到头部
/**
* 添加节点到头部
*/
public void addHead(String data) {
MyDoubleNode head = headNode;
//在头节点插入元素,上一个元素的指针肯定是 null 的
MyDoubleNode newNode = new MyDoubleNode(null, data, head);
//head 为 null,代表链表中没有元素
if(head == null) {
//尾节点也是新节点
tailNode = newNode;
} else {
//prev 指向新节点
head.prev = newNode;
}
//将新节点做为头节点
headNode = newNode;
size++;
}
add
添加节点到链表尾部
/**
* 添加节点到链表尾部
*/
public void add(String data) {
MyDoubleNode tail = tailNode;
MyDoubleNode newNode = new MyDoubleNode(tail, data, null);
tailNode = newNode;
if(tail == null) {
headNode = newNode;
} else {
tail.next = newNode;
}
size++;
}
add(int index, String data)
指定下标添加链表节点
/**
* 指定下标添加链表节点
*/
public void add(int index, String data) {
//下标越界
if(index > size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
//如果添加的是尾部元素
if(index == size) {
add(data);
} else {
//获得指定下标节点
MyDoubleNode node = getNode(index);
//获得下标节点的上个节点
MyDoubleNode prev = node.prev;
//构建新节点 新节点已经关联了上一个节点和下一个节点关系 只需要再关联 上一个节点和下一个节点 与新节点的关系
MyDoubleNode newNode = new MyDoubleNode(prev, data, node);
//将下标节点的上个指针指向新节点
node.prev = newNode;
//当 index 为 0 时,prev 是为 null 的,也就是添加头节点
//将新节点给 headNode
if(prev == null) {
headNode = newNode;
} else {
//将下标节点的上个节点的 next 指针指向新节点
prev.next = newNode;
}
size++;
}
}
//这个获得指定下标的元素的代码是可以进行优化的
//各位小伙伴可以去看看 LinkedList 的实现
private MyDoubleNode getNode(int index) {
//由于有了上一个元素的指针,可以从后向前遍历
MyDoubleNode cur = headNode;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
remove
/**
* 删除指定下标节点
*/
public void remove(int index) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
MyDoubleNode node = getNode(index);
MyDoubleNode prev = node.prev;
MyDoubleNode next = node.next;
// prev 为 null,代表 index = 0
// 将要删除节点的下一个节点作为 headNode
if(prev == null) {
headNode = next;
} else {
//跳过要删除节点的指向, 将要删除节点的上一个节点 指向 要删除节点的下一个节点
prev.next = next;
//方便GC
node.prev = null;
}
// next 为 null,代表 index 为最大的下标,也就是要删除尾部节点
// 将要删除节点的上一个节点作为 tailNode
if(next == null) {
tailNode = prev;
} else {
//跳过要删除节点的指向, 将要删除节点的下一个节点 指向 要删除节点的上一个节点
next.prev = prev;
//方便GC
node.next = null;
}
node.item = null;
size--;
}
removeHead
删除链表头节点
/**
* 删除链表头节点
*/
private void removeHead() {
//remove(0);
if(headNode == null) {
return;
}
//获得头节点的下个节点
MyDoubleNode headNext = headNode.next;
//方便GC
headNode.item = null;
headNode.next = null;
//将下个节点赋值给头节点
headNode = headNext;
//如果头节点的下个节点为 null,说明链表中就一个节点
//删除了这个节点,尾节点也要为 null
if(headNext == null) {
tailNode = null;
} else {
//链表中有多个节点,headNext 现在为 头节点,头节点的 prev 指针需要指向 null
headNext.prev = null;
}
size--;
}
removeTail
删除链表尾节点
/**
* 删除链表尾节点
*/
public void removeTail() {
// remove(size -1);
if(tailNode == null) {
return;
}
//获得尾节点的上个节点
MyDoubleNode tailPrev = tailNode.prev;
//方便GC
tailNode.item = null;
tailNode.prev = null;
//将上个节点赋值给尾节点
tailNode = tailPrev;
//如果尾节点的上个节点为 null,说明链表中就一个节点
//删除了这个节点,头节点也要为 null
if(tailPrev == null) {
headNode = null;
} else {
//链表中有多个节点,tailPrev 现在为尾节点,尾节点的 next 指针需要指向 null
tailPrev.next = null;
}
size--;
}
set
修改指定下标节点值
/**
* 修改指定下标节点值
*/
public void set(int index, String data) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
//获得指定下标节点
MyDoubleNode node = getNode(index);
node.item = data;
}
get
获得指定下标节点的值
/**
* 获得指定下标节点的值
*/
public String get(int index) {
//下标越界
if(index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + size);
}
//获得指定下标节点
MyDoubleNode node = getNode(index);
return node.item;
}
测试数据
public static void main(String[] args) {
MyDoubleLinkedList linkedList = new MyDoubleLinkedList();
System.out.println("添加到头部");
linkedList.addHead("22");
linkedList.addHead("11");
linkedList.print();
System.out.println("添加到尾部");
linkedList.add("33");
linkedList.print();
System.out.println("指定下标添加");
linkedList.add(0,"555");
linkedList.add(2,"55544");
linkedList.add(5,"666");
linkedList.print();
System.out.println("指定下标删除");
linkedList.remove(1);
linkedList.remove(1);
linkedList.print();
System.out.println("删除头尾节点");
linkedList.removeHead();
linkedList.removeTail();
linkedList.print();
System.out.println("指定下标修改节点");
linkedList.set(0,"cxyxj");
linkedList.print();
System.out.println("获得指定下标节点");
System.out.println(linkedList.get(0));
}
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。