1 单链表基础与构造方法
程序就是算法加数据结构,数据结构是算法的基础,而数据结构的基础是创建 + 增删改查。
1.1链表的内部结构
什么是链表?单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的 next 指针。表中最后一个元素的next指向null。如下图:
首先要先理解 JVM 是怎么构建出链表的。JVM 里有栈区和堆区,栈区主要存引用,也就是一个指向实际对象的地址,而堆区存的是创建的对象,我们 new 出来的都存放在堆区。那么 JVM 如何构建链表呢?我们首先定义这样一个类:
public class Node {
public int var;
public Node next;
public Node(int var) {
this.var = var;
}
}
这里的 var 就是当前结点的值,next 指向下一个结点。这里我们为了算法的简洁将其都定义为 public ,显然这违背了Java面向对象的设计要求,但是为了代码更为精简,在算法题目中我们一般都这样做,然后是单链表的实现。
/**
* 一个简单的链表实例,用于演示 JVM 怎么构造链表
*/
public class BasicLink {
public static void main(String[] args) {
int[] a = {1,2,3,4,5,6};
Node node = initLinkedList2(a);
System.out.println(node);//这里我们可以通过 debug 看一下 node 的结构,如下图:
}
private static Node initLinkedList(int[] a) {
Node head = null;
Node cur = null;
Node newNode5 = new Node(a[5]);
Node newNode4 = new Node(a[4]);
Node newNode3 = new Node(a[3]);
Node newNode2 = new Node(a[2]);
Node newNode1 = new Node(a[1]);
Node newNode0 = new Node(a[0]);
//指向列表的第一个元素
head = newNode0;
newNode0.next = newNode1;
newNode1.next = newNode2;
newNode2.next = newNode3;
newNode3.next = newNode4;
newNode4.next = newNode5;
return head;
}
这样我们就定义好了一个最基础的链表。那么如果是双向列表我们又该如何应对呢?很简单,我们只需要在 Node 类中加上一个 public Node pre; 即可。创建方法与单链表类似:
private static Node initLinkedList2(int[] a) {
Node head = null;
Node cur = null;
Node newNode5 = new Node(a[5]);
Node newNode4 = new Node(a[4]);
Node newNode3 = new Node(a[3]);
Node newNode2 = new Node(a[2]);
Node newNode1 = new Node(a[1]);
Node newNode0 = new Node(a[0]);
//指向列表的第一个元素
head = newNode0;
newNode0.next = newNode1;
newNode1.next = newNode2;
newNode2.next = newNode3;
newNode3.next = newNode4;
newNode4.next = newNode5;
newNode1.pre = newNode0;
newNode2.pre = newNode1;
newNode3.pre = newNode2;
newNode4.pre = newNode3;
newNode5.pre = newNode4;
return head;
}
扩展:我们将 next 和 pre 属性改成 left 和 right 我们就会得到一个二叉树。
1.2 遍历链表
对于单链表,不管进行什么操作,一定是从头开始逐个向后访问,所以操作之后是否还能找到表头非常重要。切记 不能只顾当前位置而将标记表头的指针丢掉了。
通过遍历得到链表的长度:
public static int getListLength(Node head){
int length = 0;
Node node = head;
while(node != null){
length++;
node = node.next;
}
return length;
}
1.3 链表插入
单链表的插入,和数组的插入一样。单链表的插入操作需要考虑三种情况:首部,中部和尾部。
1.3.1 在链表的表头插入
在链表的表头插入新结点非常简单,容易出错的是经常会忘记 head 需要重新指向表头。我们创建一个新结点 newNode,怎么连接到原来的链表上呢?执行 newNode.next = head 即可。之后我们要遍历新链表就要从 newNode 开始一直 next 了,但是我们还是习惯让 head 来表示头结点,所以让 head = newNode 就行了。
1.3.2 在链表中间插入
在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是当我们遍历到这个结点后我们却无法获取前驱结点了。
所以我们需要在目标位置的前一个位置停下来,也就是使用 cur.next 的值而不是 cur 的值来判断,这也是链表最常用的策略。例如下图,我们需要在 21 的前面插入,当 cur.next = node(21) 就应该停下,此时 cur.val = 12 .然后需要给 newNode 前后接两根线,此时只能先让 new.next = node(12).next (图中虚线),然后node(12).next = new,(注意 这里的顺序不能颠倒) 如果我们先让 node(12).next = new,那么图中 12 到 21 的线就断了,我们就无法得到 21及其后继结点了。
1.3.3 在链表的结尾插入结点
表尾插入比较简单,只需要将尾结点指向新结点就行了。
综上,我们写出链表插入的方法:
/**
* 链表插入
* @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 = getListLength(head);
if (position > size+1 || position < 1){
System.out.println("位置参数越界");
return head;
}
//表头插入
if (position == 1){
nodeInsert.next = head;
head = nodeInsert;
//这里也可以直接返回 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;
}
1.4 链表删除
删除同样分为删除头部元素,中间元素和尾部元素。
1.4.1 删除表头结点
删除表头比较简单,一般只需要执行 head = head.next 即可。此时原来的头结点不可达,会被 JVM 回收掉。
1.4.2 删除最后一个结点
找到最后一个结点的前驱结点,执行cur.next = null;同理原来的末尾结点会被回收。
1.4.3 删除中间结点
找到位置后,将 cur.next 更新为 cur.next.next 即可。
完整实现:
/**
* 删除结点
* @param head 链表头结点
* @param position 删除结点位置,取值从 1 开始
* @return 删除后的链表头结点
*/
public static Node deleteNode(Node head,int position){
if (head == null){
return null;
}
int size = getListLength(head);
//这里是 size 而不是 size+1
if (position > size || position < 1){
System.out.println("输入的参数有误");
return head;
}
if (position == 1){
head = head.next;
return head;
//可以简化为 return head.next;
}else {
//这里为了防止丢失头指针,我们不直接操作头指针来进行遍历
Node preNode = head;
int count = 1;
while(count < position - 1){
preNode = preNode.next;
count++;
}
//curNode 即需要删除的结点
Node curNode = preNode.next;
preNode.next = curNode.next;
}
return head;
}
2 双向链表设计
2.1 基本概念
双向链表顾名思义就是既可以向前,也可以向后。有两个指针的好处自然是移动元素更加方便。
双向链表的结点:
public class DoubleNode {
public int data; //数据域
public DoubleNode next;
public DoubleNode prev;
public DoubleNode(int data) {
this.data = data;
}
public void displayNode(){
System.out.println("{" + data + "}");
}
}
双向链表的结构和遍历的方法:
public class DoubleLinkList {
private DoubleNode first;
private DoubleNode last;
public DoubleLinkList(){
first = null;
last = first;
}
//从头部开始打印
public void displayForward(){
System.out.println("List(first--->last): ");
DoubleNode current = first;
while( current != null ){
current.displayNode();
current = current.next;
}
System.out.println();
}
//从尾部开始打印
public void displayBackward(){
System.out.println("List(last--->first): ");
DoubleNode current = last;
while( current != null ){
current.displayNode();
current = current.prev;
}
System.out.println();
}
}
2.2 插入元素
操作双向链表的方法头部,尾部,和中间位置有较大的区别。
//头部插入
public void insertFirst(int data){
DoubleNode newDoubleNode = new DoubleNode(data);
if (first == null){
last = newDoubleNode;
}else {//如果不是第一个结点的情况
//将还没有插入新结点之前链表的第一个结点的previous指向newNode
first.prev = newDoubleNode;
}
newDoubleNode.next = first;
first = newDoubleNode;
}
//尾部插入
public void insertLast(int data){
DoubleNode newDoubleNode = new DoubleNode(data);
if (last == null){
first = newDoubleNode;
}else {
newDoubleNode.prev = last;
last.next = newDoubleNode;
}
last = newDoubleNode;
}
在中间插入的情况:
public void insertAfter(int key,int data){
DoubleNode newDoubleNode = new DoubleNode(data);
//避免丢失头指针
DoubleNode current = first;
//遍历找到插入的位置
while((current != null) && (current.data != key)){
current = current.next;
}
//若当前结点为空
//当前结点为空有两种情况 链表为空或者找不到 key 值
if (current == null){//链表为空
if (first == null){
first = newDoubleNode;
last = newDoubleNode;
}else {//找不到 key 值,则在链表尾部插入一个新的结点
last.next = newDoubleNode;
last = newDoubleNode;
}
}else {//找到了 key 值,也分为两种情况
if (current == last){//key 值与最后结点的data相等
newDoubleNode.next = null;
last = newDoubleNode;
}else {//两结点之间插入
newDoubleNode.next = current.next;
current.next.prev = newDoubleNode;
}
current.next = newDoubleNode;
newDoubleNode.prev = current;
}
}
2.3 删除元素
2.3.1 删除首尾元素
//删除首元素
public DoubleNode deleteFirst(){
DoubleNode temp = first;
//若链表只有一个结点,删除后链表为空,将 last 指向 null。
if (first.next == null){
last = null;
}else {
//若链表有两个及以上的结点,将 first.next变成第一个结点
first.next.prev = null;
}
//将 first.next 赋给 first
first = first.next;
//返回删除的结点
return temp;
}
//删除尾部结点
public DoubleNode deleteLast(){
DoubleNode temp = last;
//如果链表只有一个结点,删除之后为空表,last 指向 null
if (first.next == null){
first = null;
}else {
//将上一个结点的 next 域指向 null
last.prev.next = null;
}
//上一个结点成为最后一个结点
last = last.prev;
return temp;
}
2.3.2 删除中间元素
//删除中间元素
public DoubleNode deleteKey(int key){
DoubleNode current = first;
//遍历链表寻找该值所在的结点
while(current != null && current.data != key){
current = current.next;
}
//若当前结点指向 null 则返回 null
if (current == null){
return null;
}else {
//如果 current 是第一个结点
if (current == first){
//则将 first 指向它,将该结点的 previous 指向 null
first = current.next;
current.next.prev = null;
}else if (current == last){
last = current.prev;
current.prev.next = null;
}else {
//当前结点的上一个结点的 next 域应指向当前的下一个结点
current.prev.next = current.next;
//当前结点的下一个结点的 previous 域应当指向当前结点的上一个结点
current.next.prev = current.prev;
}
}
return current;
}