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

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值