目录
1 单链表基础与构造
算法的基础是数据结构,其基础都是创建+增删改查,该关卡从这五项开始学习链表。
1.1 链表概念
单向链表就像铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素next指向null。如下图:
注意:一个结点只能有一个后继,但不代表一个结点只能有一个被指向。即使两个结点的值相等,也不是同一个结点。故如下图,图一满足单链表要求,图二不满足单链表要求
节点和头节点:
在链表中,每个点都由值和指向下一个结点的地址组成的独立的单元,成为一个结点(节点)。
知道单链表第一个元素就可以遍历访问整个链表,即第一个结点,一般成为头结点。
虚拟结点:
其实就是一个结点dummyNode,其next指针指向head,即dummyNode.next=head。
使用虚拟结点,要获得head结点或者从方法(函数)里返回的时候,则应使用dummyNode.next
dummyNode的val不会被使用,初始化为0或-1。
作用:方便处理首部结点
1.2 创建链表
JVM构建链表:JVM有栈区、堆区,栈区主要存应用,也就是一个指向实际对象的地址,而堆区才是创建的对象。
如下定义:
public class Course{
int val;
Course next;
}
next指向了下一个同为Course类型的对象,则结构图如下:
通过栈的引用(即地址)找到val(1),然后val(1)结点又存了指向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 void setData(int data) {
this.data = data;
}
public ListNode getNext() {
return next;
}
public void setNext(ListNode next) {
this.next = next;
}
}
LeetCode算法题常见创建方式:
public class ListNode{
private int val;
private ListNode next;
ListNode(int x){
val = x;
// 这个一般作用不大,写了会更规范
next = null;
}
}
ListNode listNode = new ListNode(1);
val就是结点的值,next指向下一个结点。由于变量为public,故创建对象后能直接使用listnode.val和listnode.next来操作。虽然违背面向对象设计要求,但代码更精简。
1.3 遍历链表
单链表遍历:从头开始访问,所以操作之后能否找到表头非常重要。
代码如下:
// 得到链表长度
public static int getListLength(ListNode head){
int length = 0;
ListNode node = head; // 定义头结点
while(node != null){
length++;
node = node.next; // 遍历完该结点后指针指向下一个结点
}
return length;
}
// 遍历打印列表
public static void printNodeList(ListNode head) {
int length = 0;
ListNode node = head; // 定义头结点
while (node != null) {
System.out.print(node.val);
length++;
node = node.next; // 遍历完该结点后指针指向下一个结点
}
}
1.4 链表插入
1.4.1 在链表表头插入
创建一个新的结点newNode,执行newNode.next=head重新指向表头,再让head=newNode重新令head指向链首元素,如下图:
1.4.2 在链表中间插入
先遍历找到要插入的位置,在目标结点的前一个位置停下来,使用cur.next的值来判断
例如下图中,要在7的前面插入,当cur.next=node(7)则须停下,此时cur.cal=15。此时先让new.next=node(15).next,使新结点指向后继结点。然后node(15).next=new,使得前驱节点指向新建结点,则完成插入,注意两个指向改变顺序不能颠倒!
1.4.3 在单链表结尾插入
只需将尾结点指向新节点即可。
综上所有链表插入的方法如下:
/**
* 链表插入
* @param head 链表头结点
* @param nodeInsert 待插入结点
* @param position 待插入位置 从1开始
* @return 插入后得到头结点
*/
public static ListNode insertNode(ListNode head, ListNode 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;
}
ListNode pNode = head;
int count = 1;
// 遍历找到目标结点前一个结点
while (count < position - 1) {
pNode = pNode.next;
count++; //
}
nodeInsert.next = pNode.next;
pNode.next = nodeInsert;
return head;
}
补充:当head=null,有两种处理,可以令插入到结点就是链表的头结点,也可以直接抛出不能插入的异常,一般偏向前者。
1.5 链表删除
1.5.1 删除表头结点
执行head=head.next即可,如下图,将head向前移动一次后,原结点不可达,会被JVM回收。
1.5.2 删除尾结点
找到其前驱结点,令其指向null即可。例如下图,同样用cur.next = 40 找到其前驱结点,再执行cur.next=null即可,此时结点40不可达,被JVM回收。
1.5.3 删除中间结点
同样用cur.next比较,找到位置后,将cur.next指针的值更新为cur.next.next即可。如下图:
链表结点删除代码实现:
/**
* 删除节点
* @param head 链表头节点
* @param position 删除节点位置,从1开始
* @return 删除后的链表头节点
*/
public static ListNode deleteNode(ListNode head, int position){
if(head == null){
return null;
}
// 元素个数
int size = getLength(head);
if (position > size || position < 1){
System.out.println("输入的参数有误");
return head;
}
// 删除头结点
if (position == 1){
// curNode就是链表新的head
ListNode node = head.next;
head.next = null;
return node;
}else {
ListNode cur = head;
int count = 1;
while(count < position - 1){
cur = cur.next;
count++;
}
cur.next = cur.next.next;
}
return head;
}
2. 双向链表
2.1 双向链表概念
前面单向链表已经知道每一个结点有且只有一个指针,指向其下一个结点。而双向链表顾名思义就是双方向的,一个结点有两个指针,一个指针指向上一个结点(前驱),另一个指针指向下一个结点(后继)。头结点pre指针和尾结点next指针都指向null。
每一个结点结构为:
2.2 创建链表
定义双向结点类型
双向链表每个结点有三部分,一部分为数据域data,其类型可自定义,也可使用泛型来定义该双结点,此处采用比较简便的方法,直接定位int类型。另外两个变量为该结点指向的前一个结点与后一个结点,并定义了他的get和set方法以及有参和午餐构造方法。
public class DoubleNode {
public int data;
public DoubleNode pre;
public DoubleNode next;
public DoubleNode(int data) {
this.data = data;
}
public DoubleNode() {
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public DoubleNode getPre() {
return pre;
}
public void setPre(DoubleNode pre) {
this.pre = pre;
}
public DoubleNode getNext() {
return next;
}
public void setNext(DoubleNode next) {
this.next = next;
}
}
2.3 遍历链表
双向链表与单向链表的遍历方法其实是一样的,所以只需知道其头结点或者尾结点,便可按顺序遍历出链表中的所有结点,再此遍不多做阐述。
2.4 链表插入
2.4.1 在链表表头插入
其实双向链表的插入与单向链表大同小异。
在此有两种情况。当链表为空时,则插入结点即为头结点,也为尾结点。当链表不为空时,则将插入结点的next指向原头结点,再令原头结点的前驱指向新结点,再把头结点设为新结点。
2.4.2 在链表中间插入
在该种情况我们可以排除链表为空的情况了,所以接下来只需找到插入位置的前一个结点cur,令新结点的next指向cur的后继,再令其后继的pre指针指向新结点。然后再令cur的next指针指向新结点,令新结点的pre指针指向前驱。
2.4.3 在链表表尾插入
这种情况和在表头插入实现一样,让尾结点next指针指向新结点,让新结点pre指针指向尾结点即可。如果有设置尾结点tail,则可令尾结点为新的结点。在此就不展示图片了,和头插法一样。
综上,所有双向链表插入的方法实现如下:
public static DoubleNode insertNode(DoubleNode head, DoubleNode 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.pre = nodeInsert;
nodeInsert.pre = null;
head = nodeInsert;
return head;
}
DoubleNode pNode = head;
int count = 1;
// 遍历找到目标结点前一个结点
while (count < position - 1) {
pNode = pNode.next;
count++; //
}
// 如果不是表尾
if(position != size + 1){
pNode.next.pre = nodeInsert;
}
nodeInsert.next = pNode.next;
pNode.next = nodeInsert;
nodeInsert.pre = pNode;
return head;
}
public static int getLength(DoubleNode head){
int length = 0;
DoubleNode node = head; // 定义头结点
while(node != null){
length++;
node = node.next; // 遍历完该结点后指针指向下一个结点
}
return length;
}
2.5 链表删除
2.5.1 删除表头结点
删除表头结点,如果只有一个结点,那么删除后链表为空。此外,只需将其后继的pre指针指向null,令新的结点为头结点,返回原头结点数值,则删除完成。
2.5.2 删除中间结点
删除中间结点,可以从头结点开始,按顺序遍历到删除结点的前一个结点,令该结点的后继指向删除结点的后继,令删除结点的后继的pre指针指向前一个结点,并返回所删除的结点的数值。
2.5.3 删除尾结点
删除尾结点只需将尾结点的前一个结点的pre指针指向null,再令其前一个结点为新的尾结点,最终返回原尾结点的数值。
综上,所有删除结点的实现代码如下:
public static DoubleNode deleteNode(DoubleNode head, int position) {
if (head == null) {
throw new RuntimeException("链表为空!");
}
// 元素个数
int size = getLength(head);
if (position > size || position < 1) {
throw new RuntimeException("输入的参数有误");
}
// 只有一个结点
if (head.next == null) {
return head;
}
// 删除头结点
if (position == 1) {
DoubleNode node = head.next;
head.next.pre=null;
return node;
}
DoubleNode cur = head;
int count = 1;
while (count < position - 1) {
cur = cur.next;
count++;
}
// 删除尾结点
if (position == size) {
int data = cur.next.data;
cur.setNext(null);
return head;
}
// 删除中间结点
DoubleNode temp = cur.next;
int data = temp.data;
cur.next = temp.next;
temp.next.pre = cur;
temp.setNext(null);
return head;
}