算法通关村第一关 —— 链表青铜挑战笔记

目录

1 单链表基础与构造

1.1 链表概念

1.2 创建链表

1.3 遍历链表

1.4 链表插入

1.4.1 在链表表头插入

1.4.2 在链表中间插入

1.4.3 在单链表结尾插入

1.5 链表删除

1.5.1 删除表头结点

1.5.2 删除尾结点

1.5.3 删除中间结点

2. 双向链表

2.1  双向链表概念

2.2 创建链表

定义双向结点类型

2.3 遍历链表

2.4 链表插入

2.5 链表删除

2.5.1 删除表头结点

2.5.2 删除中间结点

 2.5.3 删除尾结点


1 单链表基础与构造

算法的基础是数据结构,其基础都是创建+增删改查,该关卡从这五项开始学习链表。

1.1 链表概念

单向链表就像铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素next指向null。如下图:

 注意:一个结点只能有一个后继,但不代表一个结点只能有一个被指向。即使两个结点的值相等,也不是同一个结点。故如下图,图一满足单链表要求,图二不满足单链表要求

算法通关村第一关 —— 链表青铜挑战笔记_第1张图片

 节点和头节点:

在链表中,每个点都由指向下一个结点的地址组成的独立的单元,成为一个结点(节点)。

知道单链表第一个元素就可以遍历访问整个链表,即第一个结点,一般成为头结点。

虚拟结点:

其实就是一个结点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类型的对象,则结构图如下:

算法通关村第一关 —— 链表青铜挑战笔记_第2张图片

通过栈的引用(即地址)找到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指向链首元素,如下图:

算法通关村第一关 —— 链表青铜挑战笔记_第3张图片

1.4.2 在链表中间插入

先遍历找到要插入的位置,在目标结点的前一个位置停下来,使用cur.next的值来判断

例如下图中,要在7的前面插入,当cur.next=node(7)则须停下,此时cur.cal=15。此时先让new.next=node(15).next,使新结点指向后继结点。然后node(15).next=new,使得前驱节点指向新建结点,则完成插入,注意两个指向改变顺序不能颠倒!

算法通关村第一关 —— 链表青铜挑战笔记_第4张图片

1.4.3 在单链表结尾插入

只需将尾结点指向新节点即可。

算法通关村第一关 —— 链表青铜挑战笔记_第5张图片

 综上所有链表插入的方法如下:

 /**
     * 链表插入
     * @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回收。

算法通关村第一关 —— 链表青铜挑战笔记_第6张图片

1.5.2 删除尾结点

找到其前驱结点,令其指向null即可。例如下图,同样用cur.next = 40 找到其前驱结点,再执行cur.next=null即可,此时结点40不可达,被JVM回收。

算法通关村第一关 —— 链表青铜挑战笔记_第7张图片

1.5.3 删除中间结点

同样用cur.next比较,找到位置后,将cur.next指针的值更新为cur.next.next即可。如下图:

算法通关村第一关 —— 链表青铜挑战笔记_第8张图片

链表结点删除代码实现

/**
     * 删除节点
     * @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指向原头结点,再令原头结点的前驱指向新结点,再把头结点设为新结点。

算法通关村第一关 —— 链表青铜挑战笔记_第9张图片

 2.4.2 在链表中间插入

在该种情况我们可以排除链表为空的情况了,所以接下来只需找到插入位置的前一个结点cur,令新结点的next指向cur的后继,再令其后继的pre指针指向新结点。然后再令cur的next指针指向新结点,令新结点的pre指针指向前驱。

算法通关村第一关 —— 链表青铜挑战笔记_第10张图片

 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,令新的结点为头结点,返回原头结点数值,则删除完成。

算法通关村第一关 —— 链表青铜挑战笔记_第11张图片

2.5.2 删除中间结点

 删除中间结点,可以从头结点开始,按顺序遍历到删除结点的前一个结点,令该结点的后继指向删除结点的后继,令删除结点的后继的pre指针指向前一个结点,并返回所删除的结点的数值。

算法通关村第一关 —— 链表青铜挑战笔记_第12张图片

 2.5.3 删除尾结点

删除尾结点只需将尾结点的前一个结点的pre指针指向null,再令其前一个结点为新的尾结点,最终返回原尾结点的数值。

算法通关村第一关 —— 链表青铜挑战笔记_第13张图片

综上,所有删除结点的实现代码如下:

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;
}

你可能感兴趣的:(链表,数据结构)