1、链表的概念
1.1 单向链表
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干个节点node构成,每个节点node有指向下一节点的指针,从头节点开始,一个节点一个节点的连到最后一个节点,最后一个节点指向null,如下图所示:
注意,next只能指向一个后继节点,如下图所示的这种情况就不属于单链表
但是多个节点可以指向同一个节点,如下图所示:
1.2 双向链表
双向链表比单向链表稍微复杂一点,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针,如下图所示
2、链表的相关概念
节点与头节点、尾节点
在链表中,每个点都是由存放数据的变量data和指向下一节点的指针next组成的独立的单元,称之为节点(或结点)
链表的第一个节点称为头节点,在单向链表中,可以根据头节点遍历访问到整个链表
链表的最后一个节点称为尾节点,指向空值null
虚拟节点
虚拟节点dummyNode是一个next指针指向头节点的节点,即dummyNode.next = head。
虚拟节点的主要用处是方便处理头节点,否则需要在代码里单独处理头节点的问题。
在使用虚拟节点时,如果要获取head节点,或者从方法里返回时,应使用dummyNode.next
3、链表的创建
JVM是怎么样构建链表的?
在JVM中存在栈区和堆区,栈区主要存引用,也就是一个指向实际对象的地址,而堆区存的才是创建的对象。
大致的情况如下图所示:
通过栈中存储的引用可以找到val(1)节点,而val(1)节点中又存储了指向下一节点val(2)的指针,所以可以找到val(2),以此类推,就可以构造出一条链式访问结构。
Java中具体的链表定义如下:
public class ListNode {
private int data;
private ListNode next;
public ListNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public ListNode getNext() {
return next;
}
public void setData(int data) {
this.data = data;
}
public void setNext(ListNode next) {
this.next = next;
}
}
但是在LeetCode中链表经常以如下方式定义:
public class ListNode {
public int val;
public ListNode next;
ListNode(int x) {
val = x;
//这个一般用处不大,写了会更加规范
next = null;
}
}
ListNode listNode = new ListNode(1);
因为变量都是public的,所以使用起来更加方便,代码更精简,在算法题中应用广泛。
4、链表的基本操作
4.1 查找节点
对于链表来说,无法像数组那样直接使用下标定位,只能从头节点开始向后一个一个节点逐一寻找。
例如查找链表中的第三个节点:
链表中的数据只能按顺序进行访问,最坏的时间复杂度是O(n)。
查找节点的代码如下:
public static Node getNode(Node head, int index){
Node node = head;
if(index < 0 || index >= size){
System.out.println("位置参数越界");
}
for(int i = 0; i < index; i++){
node = node.next;
}
return node;
}
遍历链表和查找节点是一样的,也是根据头节点一个一个往下遍历,代码如下:
public static int getListLength(Node head){
int length = 0;
Node node = head;
while(node != null){
length++;
node = node.next;
}
return length;
}
4.2 增加节点
因为链表是线性存储结构,所以在插入的时候需要考虑节点在链表中的位置,分为三种情况考虑:头部、中部和尾部
4.2.1 在链表表头插入
链表头部插入,可以分为两个步骤:
- 把新节点的next指针指向原来的头节点。
- 把新节点变为链表的头节点。
4.2.2 在链表中间插入
在中间位置插入,分为三个步骤:
- 找到需要插入的位置
- 将新节点的next指针指向插入位置的节点
- 插入位置前置节点的next指针指向新节点
需要注意的是,我们要在目标节点的前一个位置停下来,也就是使用cur.next的值而不是cur的值来判断。因为如果使用cur来判断,就不能获得前驱节点的位置了,也就无法把新节点接进来
如下所示,如果要在7前面插入,当cur.next = node(7)了就应该停下来,然后先让新节点的next接上node(7),再把前驱节点也就是cur的next接上新节点。
如果接入的顺序颠倒了,也就是先把3接上4的话,后面从7开始的节点会丢失,因为每个节点只会有一个next。
4.2.3 在链表尾部插入
链表的尾部插入很简单,直接将链表尾节点的next指向新节点,新节点的next指向null就可以了,如下图所示
4.2.4 代码实现
/**
* 链表插入
* @param head 链表头节点
* @param nodeInsert 待插入节点
* @param position 待插入位置,从1开始
* @return 插入后得到的链表头节点
*/
public static Node insertNode(Node head, Node nodeInsert, int position){
if(head == null){
//这里可以认为待插入的节点就是链表的头节点,也可以抛出不能插入的异常
return nodeInsert;
}
//已经存放的元素个数
int size = getLength(head);
if(position > size + 1 || position < 1){
System.out.println("位置参数越界");
return head;
}
//表头插入
if (position == 1){
nodeInsert.next = head;
head = nodeInsert;
return head;
}
Node pNode = head;
int count = 1;
while(count < position - 1){
pNode = pNode.next;
count++;
}
nodeInsert.next = pNode.next;
pNode.next = nodeInsert;
return head;
}
4.3 删除节点
删除节点同样是分为头部、中部和尾部三个位置
4.3.1 删除表头节点
删除表头节点就是把链表的头节点设为原先头节点的next指向的节点,即head = head.next,如下图:
需要注意,许多高级语言,如Java,拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。
4.3.2 删除最后一个节点
删除尾部节点就是遍历链表,找到倒数第二个节点,让其的next指针指向null即可,如下图:
4.3.3 删除中间节点
删除中间节点就是把要删除节点的前置节点的next指针指向要被删除节点的后一个节点,如下图:
4.3.4 代码实现
/**
* 删除节点
* @param head 链表头节点
* @param position 删除节点位置,取值从1开始
* @return 删除后的链表头节点
*/
public static Node deleteNode(Node node, int position){
if(head == null){
return null;
}
int size = getListLength(head);
if(positon > size || position < 1){
System.out.println("输入的参数有误");
return head;
}
if(position == 1){
//curNode就是链表的新head
return head.next;
}else {
Node cur = head;
int count = 1;
while(count < position - 1){
cur = cur.next;
count++;
}
cur.next = cur.next.next;
}
return head;
}
5、双向链表的插入和删除
相比于单向链表,双向链表由于有前置指针prev,所以在进行元素的插入和删除的时候还需要前置指针的指向
5.1 双向链表插入元素
5.1.1 表头插入
表头插入就是讲新节点的next指向原来的head,原来head的prev指向新节点,新节点的prev指向null,新节点称为这个双向链表的新head。
5.1.2 中间插入
在中间插入新节点,就是把新节点的next指向要插入位置的后一个节点,即new.next = cur,新节点的prev的指向后一个节点的前置节点,即new.prev = cur.prev,这样连接后新的节点就已经在这个链表中了,之后再把后一节点的前置节点的next指向新节点,即cur.prev.next = new,把后一个节点的prev指向新节点,即cur.prev = new。
5.1.3 尾部插入
尾部插入跟头部插入类似,把双向链表的尾节点的next指向新节点,新节点的prev指向原来的尾节点,新节点的next指向null。
5.2 双向链表删除元素
5.2.1 删除表头元素
删除表头元素很简单,将原来的head的next的prev指向null,head的next指向null即可。
5.2.2 删除中间元素
删除中间元素,就是将待删除节点的next指向待删除节点的prev,待删除节点的prev指向待删除节点的next。
5.2.3 删除尾部元素
删除尾部元素,就是将待删除节点的next指向null即可。
5.3 代码实现
public class DoubleNode {
//数据
private Integer data;
//后续节点节点
private DoubleNode next;
//前驱节点
private DoubleNode previous;
// 省略set get
}
//表头插入新节点
public void addFirst(int data){
// 创建新节点
DoubleNode newNode = new DoubleNode();
// 为新节点添加数据
newNode.setData(data);
// 如果表头为空直接将新节点作为头
if (head==null){
head = newNode;
}else {
// 将新节点的前驱节点指向null(声明的时候本来就是null)
//新节点的后续节点指向表头
newNode.setNext(head);
// 将表头的前驱节点指向新节点
head.setPrevious(newNode);
// head重新赋值
head = newNode;
}
}
//尾部插入新节点
public void addLast(int data){
// 创建新节点
DoubleNode newNode = new DoubleNode();
// 为新节点添加数据
newNode.setData(data);
// 如果表头为空直接将新节点作为头
if (head==null){
head = newNode;
}else {
DoubleNode currentNode = head;
//寻找尾节点
while (currentNode.getNext()!=null){
currentNode = currentNode.getNext();
}
//表尾的后续节点指向新节点
currentNode.setNext(newNode);
//新节点的前驱节点指向表尾
newNode.setPrevious(currentNode);
}
}
//指定位置插入节点
public void add(int data, int index){
// 索引超出,非法
if (index<0 || index>length()){
System.out.println("非法索引");
return;
}
// 如果索引为0,调用addFirst方法
if (index==0){
addFirst(data);
return;
}
// 如果索引等于链表的长度,调用addLast方法
if (index==length()){
addLast(data);
return;
}
// 创建新节点
DoubleNode newNode = new DoubleNode();
// 为新节点添加数据
newNode.setData(data);
// 当前节点
DoubleNode currentNode = head;
// 定义指针
int point = 0;
// 寻找插入新节点的上一个节点A
while ((index-1)!= point){
currentNode = currentNode.getNext();
point++;
}
// 转存当前节点的后续节点
DoubleNode nextNode = currentNode.getNext();
// 当前节点的后续节点指向新节点
currentNode.setNext(newNode);
// 新接的前驱节点指向当前节点
newNode.setPrevious(currentNode);
// 新节点的后续节点指向转存的节点
newNode.setNext(nextNode);
// 转存节点的前驱节点指向新节点
nextNode.setPrevious(newNode);
}
//删除表头
public void removeFirst(){
if (length()==0){
return;
}
// 只有一个节点直接清空表头
if (length()==1){
head=null;
return;
}
// 创建一个临时节点,存储表头的后续节点
DoubleNode temNode = head.getNext();
// 将临时节点的前驱节点指向null
temNode.setPrevious(null);
// 将临时节点赋值给表头
head = temNode;
}
//删除表尾
public void removeLast(){
if (length()==0){
return;
}
// 只有一个节点直接清空表头
if (length()==1){
head=null;
return;
}
DoubleNode previousNode = head;
// 寻找尾节点的前驱节点
while (previousNode.getNext().getNext()!=null){
previousNode = previousNode.getNext();
}
previousNode.setNext(null);
}
//删除指定位置节点
public void remove(int index){
if (index<0 || index>=length()){
System.out.println("非法索引");
return;
}
// 头节点
if (index==0){
removeFirst();
return;
}
// 尾节点
if (index==(length()-1)){
removeLast();
return;
}
// 欲想删除节点的前驱节点
DoubleNode previousNode = head;
// 定义指针
int point = 0;
// 寻找新节
while ((index-1)!=point){
previousNode = previousNode.getNext();
point++;
}
// 欲想删除节点的后续节点
DoubleNode nextNode = previousNode.getNext().getNext();
// 将欲想删除节点的前驱节点的后续节点指向欲想删除节点的后续节点
previousNode.setNext(nextNode);
// 将欲想删除节点的后续节点的前驱节点指向欲想删除节点的前驱节点
nextNode.setPrevious(previousNode);
}