java数据结构与算法第四课——链表

目录

一:引入

二:链表

2.1链表的概念

2.2链表的分类

三:单向链表的实现

3.1代码

3.2具体分析及部分操作详解

3.2.1头插法

3.2.2尾插法

3.3.3删除所有值为key的节点

四:LinkedList的模拟实现

4.1代码

 4.2具体分析及部分操作详解

4.2.1头插法

4.2.2删除第一次出现关键字为key的结点

五:LinkedList的使用(重点)

5.1LinkedList简介

5.2LinkedList使用

5.2.1LinkedList构造

 5.2.2LinkedList的常用方法

 5.3LinkedList遍历

 六:ArrayList和LinkedList的区别


一:引入

    上一课中我们介绍了顺序表,由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合任意位置插入和删除比较多的场景。

为了解决这一问题,java集合中又引入了LinkedList,即链表结构。

二:链表

2.1链表的概念

        链表是一种物理存储结构上非连续存储的结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

 

        链表容易理解,相比于顺序表,链表的每个节点多了一个元素,用于存储下一个节点的地址。这样,我们就可以通过这一地址寻找到当前节点的下一个节点,即将所有的节点都串联起来,链式访问,故名“链表”。

2.2链表的分类

        链表的不同类型,可以这样描述:

 

         通过排列组合,我们可以得到8种不同类型的链表,其中重点需要掌握的,是如下两种:

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。同时也多见于面试题中。

2.无头双向链表:在Java的集合框架库中,LinkedList底层实现就是无头双向循环链表,足见其重要性,非同小可。

三:单向链表的实现

3.1代码

         在实现单链表过程中,创建了如上图所示的两个类,MySingleList class,里面实现了所有对链表所进行的操作;Test class,用于对代码逻辑进行测试。具体代码如下:

MySingleList.java

package MySingleList;

import java.util.List;

public class MySingleList {
    static class ListNode {
        public int val;//数值域
        public ListNode next;//存储下一个节点的地址

        public ListNode(int val) {
            this.val = val;
        }
    }


    public ListNode head;//代表单链表头结点的引用

    public void display() {
        ListNode cur = this.head;
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        while (cur.next != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.print(cur.val);
        System.out.println();

    }

    //头插法
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }

    //尾插法
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        ListNode cur = this.head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }


    //任意位置插入
    public void addIndex(int index, int data) {
        ListNode node = new ListNode(data);
        if(index == 0){
            addFirst(data);
            return;
        }
        if (checkIndexAdd(index) == true) {
            ListNode cur = findIndexSubOne(index);
            node.next = cur.next;
             cur.next = node;
        }
    }

    private ListNode findIndexSubOne(int index) {
        ListNode cur = this.head;
        while((index-1) != 0){
            cur = cur.next;
            index--;
        }
        return cur;
    }



    private boolean checkIndexAdd(int index){
        if(index < 0||index > size()){
            System.out.println("你个老六,下标都不对!");
            return false;
        }
        return true;
    }

    //查找关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = this.head;
        while(cur.next != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        if(cur.val == key){
            return true;
        }
        return false;

    }

    //删除第一次出现关键字key的节点
    public void remove(int key){
        ListNode cur = this.head;
        if(cur.val == key){
            head = cur.next;
            return;
        }
        while(cur.next != null){
            if(cur.next.val == key){
                cur.next = cur.next.next;
                return;
            }
            cur = cur.next;
        }
        System.out.println("没这值!");
    }

    //删除所有值为key的节点
    public void removeAllkey(int key){
        if(head == null){
            return;
        }
        ListNode cur = head;
        ListNode curNext = cur.next;
        while(curNext != null){
            if(curNext.val == key){
                cur.next = curNext.next;
                curNext = curNext.next;
            }else{
                cur = cur.next;
                curNext = curNext.next;
            }
        }

        if(head.val == key) {
            head = head.next;
        }
    }

    //得到单链表的长度
    public int size(){
        ListNode cur = this.head;
        int count = 1;
        while(cur.next != null){
            cur = cur.next;
            count++;
        }
        return count;
    }

    //清空单链表
    public void clear(){
        this.head = null;
    }

}



 Test.java

package MySingleList;

public class Test{
    public static void main(String[] args){
        MySingleList mySingleList = new MySingleList();
        mySingleList.addFirst(1);
        mySingleList.addFirst(1);
        mySingleList.addFirst(1);
        mySingleList.addFirst(5);
        mySingleList.addFirst(1);
        mySingleList.addFirst(1);
        //mySingleList.addIndex(0,88);
//         if(mySingleList.contains(0) == true){
//             System.out.println("存在该值");
//         }
        mySingleList.removeAllkey(1);
        //mySingleList.clear();
        mySingleList.display();

    }
}

3.2具体分析及部分操作详解

3.2.1头插法

    //头插法
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }

        无论原先链表中是否有元素,进行头插法的方式都是这样朴实无华,只需将待插入节点的next指向当前链表的head,然后将该节点赋值给head即可。图解如下:

3.2.2尾插法

    //尾插法
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        ListNode cur = this.head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

       讲解尾插法,是因为这里使用了一个cur结点。当我们需要进行尾插时,必须找到最后一个结点。为了不改变头节点head的位置,我们创建一个新的结点cur指向head,让cur移动,寻找最后一个结点,并完成插入。这种思想将会不断运用于我们之后的代码中。

3.3.3删除所有值为key的节点

 //删除所有值为key的节点
    public void removeAllkey(int key){
        if(head == null){
            return;
        }
        ListNode cur = head;
        ListNode curNext = cur.next;
        while(curNext != null){
            if(curNext.val == key){
                cur.next = curNext.next;
                curNext = curNext.next;
            }else{
                cur = cur.next;
                curNext = curNext.next;
            }
        }

        if(head.val == key) {
            head = head.next;
        }
    }

        要删除所有值为key的结点,我们首先考虑这个待删除结点在链表的中间。

         如果我们要删除值为23的结点,我们需要找到该结点的前一个结点,并改变其指向,让它指向待删除结点的下一个结点。所以这里定义了两个结点,一个cur,一个curNext。 

        如图所示,其实只需要第一条代码即可将该节点删除,而第二条代码的作用是使curNext后移,便于继续进行“排查”,以期删除所有值为key的结点。 

        显然当循环结束时,我们已经可以搞定除头结点外的全部结点了。因为curNext是从第二个节点开始判断的,所以头节点被忽略了。此时,我们只需要对当前头节点中的元素值进行判断即可,如果是值为key的结点,就执行head=head.next,将头结点后移一个即可。

        在Test.java文件中对这部分逻辑进行测试,结果如下:

package MySingleList;

public class Test{
    public static void main(String[] args){
        MySingleList mySingleList = new MySingleList();
        mySingleList.addFirst(1);
        mySingleList.addFirst(1);
        mySingleList.addFirst(23);
        mySingleList.addFirst(1);
        mySingleList.addFirst(24);
        mySingleList.addFirst(8);
        mySingleList.removeAllkey(1);
        mySingleList.display();

    }
}

运行结果如下:

        测试任务非常简单,在此就不再赘述了。

四:LinkedList的模拟实现

        LinkedList底层就是一个双向链表,我们来实现一个双向链表。

双向链表图示:

4.1代码

MyLinkedList.java

public class MyLinkedList {
    static class ListNode {
        public int val;
        public ListNode prev;//前驱
        public ListNode next;//后继
 
        public ListNode(int val) {
            this.val = val;
        }
    }
 
    public ListNode head;//标记头
    public ListNode last;//标记尾巴
 
 
    public void display(){
        ListNode cur = head;
        while(cur != null){
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }
 
    //判断下标位置合法性
    public boolean checkIndex(int index){
        if(index <0 | index > size()){
            System.out.println("下标位置不合法!");
            return false;
        }
        return true;
    }
    //头插法
    public void addFirst(int data){
        ListNode cur = new ListNode(data);
        if(!(size()==0)){
            cur.next = head;
            head.prev = cur;
            head = cur;
        }else{
            head = cur;
            last = cur;
        }
    }
 
    //尾插法
    public void addLast(int data){
        ListNode cur = new ListNode(data);
        if(!(size()==0)){
            last.next = cur;
            cur.prev = last;
            last = cur;
        }else{
            head = cur;
            last = cur;
        }
    }
 
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        if(!checkIndex(index)){
            return;
        }
 
        ListNode cur = new ListNode(data);
        if(index == 0){
            addFirst(data);
            return;
        }
        if(index == size()){
            addLast(data);
            return;
        }
        //在中间插入,需要改变四个指向
        ListNode curprev = head;
        while((index-1) != 0){
            curprev = curprev.next;
            index--;
        }
        //此时curprev已经指向待插入位置的前一个结点
        cur.next = curprev.next;
        curprev.next.prev = cur;
        curprev.next = cur;
        cur.prev = curprev;
    }
 
 
    //查找是否包含关键字key是否在双向链表当中
    public boolean contains(int key){
        ListNode cur = head;
        while(cur.next != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        if(cur.val == key) return true;
        return false;
    }
 
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        if(head.val == key){
        head = head.next;
        return;     //删完走人
    }
        ListNode cur = head;
        ListNode curNext = cur.next;
 
        while(curNext != null){
            if(curNext.val == key){
                cur.next = curNext.next;
                if(curNext.next != null){
                    curNext.next.prev = cur;
                    return;     //删完走人
                }
            }
            cur = cur.next;
            curNext = curNext.next;
        }
    }
 
    //删除所有值为key的节点
    public void removeAllKey(int key){
        //先判断头节点
        if(head.val == key){
        head = head.next;
    }
        ListNode cur = head;
        ListNode curNext = cur.next;
 
        while(curNext != null){
            if(curNext.val == key){
                cur.next = curNext.next;
                if(curNext.next != null){
                    curNext.next.prev = cur;
                }
                curNext = cur.next;
            }else{
                cur = cur.next;
                curNext = curNext.next;
            }
        }
    }
 
    //得到单链表的长度
    public int size(){
        int count = 1;
        ListNode cur = head;
        if(cur == null){
            return 0;
        }
        while(cur.next != null){
            cur = cur.next;
            count++;
        }
        return count;
    }
 
    public void clear(){
        ListNode cur = head;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curNext;
        }
        head = null;
        last = null;
        }
}

 Test.java

package MyLinkedList;

public class Test {
    public static void main(String[] args) {
        MyLinkedList linkedList = new MyLinkedList();
        linkedList.addLast(12);
        linkedList.addLast(23);
        linkedList.addLast(34);
        linkedList.addLast(45);
        linkedList.addLast(56);
        linkedList.display();
        System.out.println("==========");
        linkedList.removeAllKey(12);
        linkedList.display();
        System.out.println("==========");
        linkedList.clear();
        linkedList.display();

    }
}

 4.2具体分析及部分操作详解

4.2.1头插法

 //头插法
    public void addFirst(int data){
        ListNode cur = new ListNode(data);
        if(!(size()==0)){
            cur.next = head;
            head.prev = cur;
            head = cur;
        }else{
            head = cur;
            last = cur;
        }
    }

        在进行头插法时,需要考虑当前链表是否为空。若当前链表为空,那么head和last结点将指向同一个位置;否则,我们需要同时考虑next与prev的指向,最后将head结点放在链表的开头。尾插法、在任意位置插入结点,其实都遵循相同的思路,其核心就是next和prev的指向问题。

4.2.2删除第一次出现关键字为key的结点

//删除第一次出现关键字为key的节点
    public void remove(int key){
        if(head.val == key){
        head = head.next;
        return;     //删完走人
    }
        ListNode cur = head;
        ListNode curNext = cur.next;

        while(curNext != null){
            if(curNext.val == key){
                cur.next = curNext.next;
                if(curNext.next != null){
                    curNext.next.prev = cur;
                    return;     //删完走人
                }
            }
            cur = cur.next;
            curNext = curNext.next;
        }
    }

思路:

1.当头结点中数值就是key时,我们只需将头节点向后移动一位,即head=head.next,值得注意的是在移动结束后,一定要return,因为我们只需要删除第一次出现关键字为ke的结点,此时return,可以避免误删更多的结点。

2.考虑中间结点的删除。假设curNext结点是我们需要删除的结点,那么我们必须找到它的前一个结点cur,所以这里我们用到两个节点。如果curNext结点的关键字不是key,就让cur和curNext结点分别向后移动一位;当curNext结点的关键字为key时,改变相关结点的指向即可。当成功删除后,也要进行return。

测试逻辑在此不做赘述。

五:LinkedList的使用(重点)

5.1LinkedList简介

        LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

【说明】
1. LinkedList实现了List接口;
2. LinkedList的底层使用了双向链表
3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问

5.2LinkedList使用

5.2.1LinkedList构造

        具体示例如下:


public class TestDemo1 {
    public static void main(String[] args) {
        List<Integer> list1 = new LinkedList<>();
        List<Integer> list2 = new LinkedList<>(list1);
    }
}

 5.2.2LinkedList的常用方法

 具体实例如下:

package LinkedList;

import java.util.LinkedList;
import java.util.List;

public class TestDemo1 {
    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();
        list.add(1); // add(elem): 表示尾插
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        System.out.println(list.size());
        System.out.println("=========");
        // 在起始位置插入0
        list.add(0, 0); // add(index, elem): 在index位置插入元素elem
        System.out.println(list);

        if(list.contains(1)){
            System.out.println("存在该元素!");
        }

        System.out.println("=========");

        List<Integer> copy = list.subList(0,3);
        System.out.println(copy);
    }
}

运行结果如下:

 5.3LinkedList遍历

        我会介绍这些方法:直接sout输出,fori循环,foreach循环,以及使用迭代器。话不多说,直接上代码:

public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(1); // add(elem): 表示尾插
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
//1.sout输出
        System.out.println(list);
        System.out.println();

//2.fori遍历
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i)+" ");
        }
        System.out.println();

//3.foreach遍历
        for (int e:list) {
            System.out.print(e + " ");
        }
        System.out.println();


// 4.使用迭代器遍历---正向遍历
        ListIterator<Integer> it = list.listIterator();
        while(it.hasNext()){
            System.out.print(it.next()+ " ");
        }
        System.out.println();


//5.使用反向迭代器---反向遍历
        ListIterator<Integer> rit = list.listIterator(list.size());
        while (rit.hasPrevious()){
            System.out.print(rit.previous() +" ");
        }
        System.out.println();
    }

运行结果如下:

 六:ArrayList和LinkedList的区别

面试题 : 谈谈ArrayList和LinkedList的区别 ?

1.ArrayList是顺序表 , LinkedList是链表 ;

2.ArrayList支持随机访问 , 比如我要找顺序表中下标为5的元素 , 直接就是array[5] ; LinkedList显然没这功能 .

3.查找元素get : ArrayList查找元素的时间复杂度为O(1) , LinkedList因为有遍历操作 , 所以其查找元素的时间复杂度为O(N) .

4.尾插法 : 二者的时间复杂度都是O(1) .

5.任意位置插入 : ArrayList的时间复杂度是O(N) , 因为插入位置后面的所有元素 , 都要向后移位 ; 理论上 , 链表插入元素的时间复杂应该是O(1) , 因为只需要改变节点的前后指向即可 . 但是java标准库在设计LinkedList时出现了问题 , 导致没有发挥出链表的优势 . 在进行add(int index,E element)时 , 需要先遍历找到待插入的位置 , 然后将新节点插入进去 . 由于遍历找位置和插入是一个整体 , 这就导致LinkedList在插入元素时 , 时间复杂度是O(N) .

6.删除元素 : ArrayList删除元素 , 删除位置后面的所有元素 , 都要向前移位 , 所以时间复杂度为O(N) ; LinkedList删除元素 , 无论是按照下标删除还是按照值删除,都需要先遍历链表找到要删除的节点  , 所以其时间复杂度也是O(N) .

7.综上所述 , ArrayList的优势是支持随机访问 , 且查找元素比较快 ; 而有人一整谈什么 , 如果需要频繁在任意位置插入和删除元素,就使用链表 , 这是不对的 , 使用链表插入/删除高效 , 但使用LinkedList插入/删除 , 不高效 ! 因为有一个遍历的操作 !

        到此为止,有关链表的知识,我已介绍完毕。所谓“纸上得来终觉浅,绝知此事要躬行”。下一篇将会更新链表相关的面试题,帮助我们更好地掌握这部分内容!

本课内容结束!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值