算法学习笔记——链表学习

链表是一种最基本的结构,普通的单链表就是只给你一个指向链表头的指针head,如果想访问其他元素,就只能从head开始一个个向后找,遍历链表最终会在访问尾结点之后如果继续访问,就会返回null。
在工程应用,极少见到普通单链表,比较多的是带头结点的单链表和双向循环链表。 有时候会将多个链表组合从而实现更丰富的功能,这种操作在很多底层软件里大量使用,例如操作系统、虚拟机等。

1.单向链表

1.1 链表的内部结构

首先看一下什么是链表?使用链表存储数据,不强制要求数据在内存中集中存储,各个元素可以分散存储在内存中。例如,使用链表存储 {4,,15,,7,,40},各个元素在内存中的存储状态可能是:如下图:

image.png

显然,我们只需要记住元素 4 的存储位置,通过它的指针就可以找到元素 15,通过元素 15 的指针就可以找到元素 7,以此类推,各个元素的先后次序一目了然。
可以看到,数据不仅没有集中存放,在内存中的存储次序也是混乱的。那么,链表是如何存储数据间逻辑关系的呢?链表存储数据间逻辑关系的实现方案是:为每一个元素配置一个指针,每个元素的指针都指向自己的直接后继元素,也就是上图图所示的样子。
像上图这样,数据元素随机存储在内存中,通过指针维系数据之间“一对一”的逻辑关系,这样的存储结构就是链表。
我们来看如何构造链表,
链表的基本单位是结点,有些地方也叫节点,都是一个意思,在我们的讲义里也是混着用的。
在链表中,每个结点数据元素都配有一个指针,这意味着,链表上的每个“元素”都长下图这个样子:

image.png

数据域用来存储元素的值,指针域用来存放指针。数据结构中,通常将上图这样的整体称为结点
也就是说,链表中实际存放的是一个一个的结点,数据元素存放在各个结点的数据域中。举个简单的例子,图 2 中 {1,2,3} 的存储状态用链表表示,如下图所示:

image.png

public class BasicLink {

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        Node head = initLinkedList(arr);
        System.out.println(head.next.data); //2
    }


    /**
     * 初始化一个链表
     *
     * @param arr
     * @return 头结点
     */
    private static Node initLinkedList(int[] arr) {
        Node head = null, cur = null;
        for (int i = 0; i < arr.length; i++) {
            Node newNode = new Node(arr[i]);
            newNode.next = null;
            if (i == 0) {
                head = newNode;
            } else {
                cur.next = newNode;

            }
            cur = newNode;
        }
        return head;
    }


    /**
     * 定义一个结点
     */
    static class Node {
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
            next = null;
        }
    }
}

1.2 遍历链表

对于单链表,不管进行什么操作,一定是从头开始逐个向后访问,所以操作之后是否还能找到表头非常重要。一定要注意"狗熊掰棒子"问题,也就是只顾当前位置而将标记表头的指针丢掉了。

image.png

    /**
     * 打印链表
     * @param head
     */
    private static void printLink(Node head) {
        Node temp = head; //temp指针用来遍历链表
        while (temp != null) {
            System.out.print(temp.data + " ");
            temp = temp.next;
        }
        System.out.println();
    }

    /**
     * 获取链表长度
     * @param head
     * @return length
     */
    private static int getLength(Node head) {
        int length = 0;
        while (head != null) {
            length++;
            head = head.next;
        }
        return length;
    }

测试结果:

public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        Node head = initLinkedList(arr);
        printLink(head); //1 2 3 4 5 6 7 8 9 10
    }

1.3 链表的插入

单链表的插入,和数组的插入一样,过程不复杂,但是在编码时会发现处处是坑。单链表的插入操作需要要考虑三种情况:首部、中部和尾部。

(1) 在链表的表头插入

链表表头插入新结点非常简单,容易出错的是经常会忘了head需要重新指向表头。 我们创建一个新结点newNode,怎么连接到原来的链表上呢?执行newNode.next=head即可。之后我们要遍历新链表就要从newNode开始一路next向下了是吧,但是我们还是习惯让head来表示,所以让head=newNode就行了,如下图:

image.png

(2)在链表中间插入

在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是到了该位置之后我们却不能获得前驱结点了,也就无法将结点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。
为此,我们要在目标结点的前一个位置停下来,也就是使用cur.next的值而不是cur的值来判断,这是链表最常用的策略。
例如下图中,如果要在7的前面插入,当cur.next=node(7)了就应该停下来,此时cur.val=15。然后需要给newNode前后接两根线,此时只能先让new.next=node(15).next(图中虚线),然后node(15).next=new,而且顺序还不能错。
想一下为什么不能颠倒顺序?
由于每个节点都只有一个next,因此执行了node(15).next=new之后,结点15和7之间的连线就自动断开了,如下图所示:

image.png

(3)在单链表的结尾插入结点

表尾插入就比较容易了,我们只要将尾结点指向新结点就行了。

image.png

🖥️代码实现
public class BasicLinkedList {

    public static void main(String[] args) {
        BasicLinkedList basicLinkedList = new BasicLinkedList();
        basicLinkedList.insert(1, 0);
        basicLinkedList.insert(2, 1);
        basicLinkedList.insert(3, 1);
        basicLinkedList.insert(4, 2);
        printLink(basicLinkedList.head);

    }

    // 头指针
    private Node head;
    // 尾指针
    private Node tail;
    // 链表长度
    private int size;

    /**
     * 链表的插入
     *
     * @param data  插入的元素
     * @param index 插入的位置
     */
    public void insert(int data, int index) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("index is illegal");
        }
        Node newNode = new Node(data);
        if (size == 0) {
            // 插入头部
            head = newNode;
            tail = newNode;
        } else if (size == index) {
            // 插入尾部
            tail.next = newNode;
            tail = newNode;
        } else {
            // 插入中间
            Node cur = head;
            for (int i = 0; i < size; i++) {
                if (i == index-1) {
                    newNode.next= cur.next;
                    cur.next = newNode;
                    break;
                } else {
                    cur = cur.next;
                }
            }
        }
        size++;
    }


    /**
     * 链表结点
     */
    private static class Node {
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
            next = null;
        }
    }

    /**
     * 打印链表
     * @param head
     */
    private static void printLink(Node head) {
        Node temp = head; //temp指针用来遍历链表
        while (temp != null) {
            System.out.print(temp.data + " ");
            temp = temp.next;
        }
        System.out.println();
    }

}

1.4 链表的删除

删除同样分为在删除头部元素,删除中间元素和删除尾部元素。

(1)删除表头结点

删除表头元素还是比较简单的,一般只要执行head=head.next就行了。如下图,将head向前移动一次之后,原来的结点不可达,会被JVM回收掉。

image.png

(2)删除最后一个结点

删除的过程不算复杂,也是找到要删除的结点的前驱结点,这里同样要在提前一个位置判断,例如下图中删除40,其前驱结点为7。遍历的时候需要判断cur.next是否为40,如果是,则只要执行cur.next=null即可,此时结点40变得不可达,最终会被JVM回收掉。

image.png

(3)删除中间结点

删除中间结点时,也会要用cur.next来比较,找到位置后,将cur.next指针的值更新为cur.next.next就可以解决,如下图所示:

image.png

🖥️代码实现
    /**
     * 链表的删除
     *
     * @param index 删除的位置
     * @return 删除的节点
     */
    public Node remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("index is illegal");
        }
        Node removeNode = null;
        if (index == 0) {
            // 删除头结点
            removeNode = head;
            head = head.next;
        } else if (index == size - 1) {
            // 删除尾结点
            Node cur = head;
            for (int i = 0; i < size; i++) {
                if (i == size - 2) {
                    removeNode = cur.next;
                    cur.next = null;
                    tail = cur;
                    break;
                } else {
                    cur = cur.next;
                }
            }
        } else {
            // 删除中间结点
            Node cur = head;
            for (int i = 0; i < size; i++) {
                if (i == index - 1) {
                    removeNode = cur.next;
                    cur.next = cur.next.next;
                    break;
                } else {
                    cur = cur.next;
                }
            }
        }
        size--;
        return removeNode;
    }

2. 双向链表设计

2.1 基本概念

双向链表顾名思义就是既可以向前,也可以向后。有两个指针的好处自然是移动元素更方便。

image.png

/**
  * 双向链表的结点
  */
class DoubleNode {
    int data;
    DoubleNode next;
    DoubleNode prev;

    public DoubleNode(int data) {
        this.data = data;
    }
}

双向链表的定义及遍历:

public class DoubleLinkedList {

    private DoubleNode head;
    private DoubleNode tail;

    public DoubleLinkedList() {
        head = null;
        tail = null;
    }

    // 从头打印
    public void printForward() {
        System.out.println("List(head--->tail):");
        DoubleNode cur = head;
        while (cur != null) {
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    // 从尾打印
    public void printBackward() {
        System.out.println("List(tail--->head):");
        DoubleNode cur = tail;
        while (cur != null) {
            System.out.print(cur.data + " ");
            cur = cur.pre;
        }
        System.out.println();
    }
}

2.2插入元素

(1)头尾插入
//头部插入
public void insertFirst(int data) {
    DoubleNode newNode = new DoubleNode(data);
    if (head == null) {
        head = newNode;
        tail = newNode;
    } else {
        newNode.next = head;
        head.pre = newNode;
        head = newNode;
    }
}
//尾部插入
public void insertLast(int data) {
    DoubleNode newNode = new DoubleNode(data);
    if (head == null) {
        head = newNode;
        tail = newNode;
    } else {
        newNode.pre = tail;
        tail.next = newNode;
        tail = newNode;
    }
}
(2)从某个元素后面插入
/**
   * 某个结点后面插入
   * @param data 插入的值
   * @param key 被插入的值
   */
public void insertAfter(int data, int key) {
    DoubleNode newNode = new DoubleNode(data);
    DoubleNode cur = head;
    while ((cur != null)&&(cur.data != key)) {
        cur = cur.next;
    }
    //若当前结点cur为空
    if (cur == null) {
        if (head == null) {//1.链表为空
            head = newNode;
            tail = newNode;
        } else {//2.找不到key值
            tail.next = newNode;
            newNode.pre = tail;
            tail = newNode;
        }
    }else {
        if (cur == tail){//3.找到key值,分两种情况
            //key值与最后结点的data相等
            newNode.next = null;
            tail = newNode;
        }else {
            //两结点中间插入
            newNode.next = cur.next;
            cur.next.pre = newNode;
        }
        cur.next = newNode;
        newNode.pre = cur;
    }
}

2.3删除元素

双向链表的不足就是增删改的时候,需要修改的指针多了,操作更麻烦了。由于双向链表在算法中不是很重要,我们先看一下删除的大致过程。首尾元素的删除还比较简单,直接上代码:

(1)删除头尾
// 删除头结点
public DoubleNode deleteFirst() {
    DoubleNode removedNode = head;
    // 若链表只有一个结点,删除后链表为空,将tail指向null
    if (head.next == null) {
        tail = null;
    } else {
        // 若链表有两个以上的结点,头结点删除,则head.next将变成第一个结点
        head.next.pre = null;
    }
    head = head.next;
    return removedNode;
}

//删除尾结点
public DoubleNode deleteLast() {
    DoubleNode removedNode = tail;
    // 若链表只有一个结点,删除后链表为空,将tail指向null
    if (head.next == null) {
        head = null;
    } else {
        // 若链表有两个以上的结点,将上一个结点的next域指向null
        tail.pre.next = null;
    }
    // 前一个结点变成尾结点
    tail = tail.pre;
    return removedNode;
}
(2)删除某个位置结点

我们再看删除中间元素的情况,要标记出几个关键结点的位置,也就是图中的cur,cur.next和cur.prev结点。由于在双向链表中可以走回头路,所以我们使用cur,cur.next和cur.prev任意一个位置都能实现删除。假如我们就删除cur,图示是这样的:

image.png

我们只需要调整两个指针,一个是cur.next的prev指向cur.prev,第二个是cur.prev的next指向cur.next。此时cur结点没有结点访问了,根据垃圾回收算法,此时cur就变得不可达,最终被回收掉,所以这样就完成了删除cur的操作。想一下,这里调整两条线的代码是否可以换顺序?

可以,并没有影响当前结点的指针

/**
  * 删除某个位置的结点
  * 
  * @param key 要删除的值
  * @return 被删除的结点
  */
public DoubleNode deleteKey(int key) {
    DoubleNode cur = head;
    //遍历链表寻找该值所在的结点
    while ((cur != null) && (cur.data != key)) {
        cur = cur.next;
    }
    //若当前结点指向null则返回null
    if (cur == null) {
        return null;
    } else {
        if (cur == head) {
            deleteFirst();
        } else if (cur == tail) {
            deleteLast();
        } else {
            cur.pre.next = cur.next;
            cur.next.pre = cur.pre;
        }
        return cur;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值