【DS】3.顺序表&链表万字全总结!

本文深入探讨Java中List接口的概念及其重要实现ArrayList的特点与用法,包括ArrayList的内部实现、常用方法、扩容机制等,同时对比了ArrayList与LinkedList的不同之处。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Java的集合类一般都在util包下

一、关于List

为什么要提List

有人可能会问,你不是要总结顺序表和链表吗?提List干什么?这是因为顺序表和链表背后的集合ArrayList和LinkedList都是实现了List的,我们要想学明白ArrayList和LinkedList,必须对List有一定的了解。

什么是List

站在数据结构的角度来看,List就是一个线性表,是n个具有相同类型元素的有限序列,在该序列上进行增删查改以及变量等操作

从Java内置的集合框架角度看,List是一个接口,List继承于Collection,Collection继承于Iterable。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KFLaJphg-1665031685336)(F:\typora插图\image-20221006110540998.png)]

他规定了后边容器的一些常见的方法,具体有【了解即可】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUwoX2Gt-1665031685338)(F:\typora插图\image-20221006110910971.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iiatkLBb-1665031685338)(F:\typora插图\image-20221006111701101.png)]

List怎么用

由于List是一个接口,所以不能直接用来实例化。

我们一般是创建一个实现它的类的对象(比如说ArrayList或者LinkedList的)然后用List类型的引用接收。

二、关于线性表

定义:线性表就是n个具有相同特性的数据元素的有限序列。

两种存储结构:线性存储、链式存储

要点:逻辑一定相邻,物理不一定相邻。

原因:顺序表是用数组实现的、物理上一定连续,链表是用内部类实现的,物理上不一定连续。

常见的线性表:顺序表、链表、栈、队列

三、关于顺序表

什么是顺序表

定义:顺序表是使用一段物理地址连续的存储单元存储数据元素的线性结构。

关于顺序表和数组关系的理解:顺序表实现方式就是数组,但是可以将顺序表理解成一种功能性的数据结构,是带增删查改等常用操作的,而数组只是一种单纯的数据类型。

**关于顺序表和ArrayList的理解:**顺序表是一个逻辑概念(功能性的数据结构),而ArrayList是一个集合类,是具体实现它逻辑的代码集合。

顺序表我们常用的功能:

  1. 打印顺序表【show/display】
  2. 新增元素,默认在末尾【增】
  3. 在pos位置新增元素【增】
  4. 判断是不是包含某个元素【查】
  5. 查找某个元素对应的位置【查】
  6. 获取pos位置的元素【查】
  7. 给pos位置的元素设置成为val【改】
  8. 删除第一次出现的关键字key【删】
  9. 获取顺序表的长度【查】
  10. 清空顺序表【删】

ArrayList的简化模拟实现:

我们已经知道,顺序表背后的集合是ArrayList,所以我们在这里自己模拟实现顺序表的功能名字,可以写成MyArrayList。

public class MyArrayList {
    //首先我们的顺序表必须要有一个数组
    //其次需要有usedSize,记录有效的数据个数
    //再然后我们需要给一个构造方法,一般传数组大小,所以我们的构造方法里弄大小
    //这些变量我们最好设置成为private
    private int[] elem;
    private int usedSize;
    public MyArrayList(int size){
        elem= new int[size];
    }

    //打印函数
    public void display() {
        for (int i = 0; i < usedSize; i++) {
            System.out.print(this.elem[i]+" ");
        }
        System.out.println();
    }
    //尾插元素
    public void add(int data) {
        //1.满了
        if(isFull()){
            this.elem=
                    Arrays.copyOf(this.elem,2*elem.length);
        }
        //2.没满
        this.elem[usedSize++]=data;
    }
    //判满
    public boolean isFull() {
        return this.elem.length<=usedSize;
    }
    //指定位置插入
    public void add(int pos, int data) {//插入数据需要挪动数据
        //1.坐标不合法【负数/间隔/超过数组长度】
        if(pos<0||pos>usedSize){
            throw new PosWrongfulException("坐标位置不合法,插入失败!");//自定义类不再展示
        }
        //2.如果满了,扩容
        if(isFull()){
            this.elem=
                    Arrays.copyOf(this.elem,2*elem.length);
        }
        //3.位置合法
        for (int i = usedSize; i >pos ; i--) {
            elem[i]=elem[i-1];
        }
        elem[pos]=data;
        usedSize++;
    }
    // 判定是否包含某个元素
    public boolean contains(int toFind) {
        //遍历看等不等就可以了,判不判都无所谓
        for (int i = 0; i < usedSize; i++) {
            if(toFind==this.elem[i]){
                return true;
            }
        }
        //如果是引用类型,比较内容用equals
        return false;
    }
    // 查找某个元素对应的位置
    public int indexOf(int toFind) {
        //直接遍历
        for (int i = 0; i < usedSize; i++) {
            if(this.elem[i]==toFind){
                return i;
            }
        }
        return -1;
    }
    // 获取 pos 位置的元素
    public int get(int pos) {//没有判空
        //1.位置不合法
        if(pos<0||pos>=usedSize){
            throw new PosWrongfulException("坐标位置不合法");
        }
        //2.判断是不是空的
        if(isEmpty()){
            throw new EmptyException("当前顺序表为空!");
        }
        //3.合法,返回对应的值
        return this.elem[pos];
    }
    //判空
    public boolean isEmpty() {
        return usedSize==0;
    }
    // 给 pos 位置的元素 更新 为 value
    public void set(int pos, int value) {
        //1.位置不合法
        if(pos<0||pos>=usedSize){
            throw new PosWrongfulException("坐标位置不合法,插入失败!");
        }
        //2.空了,没有更新这一说法
        if(isEmpty()){
            throw new EmptyException("当前顺序表为空!");
        }
        //3.正常更新
        this.elem[pos]=value;
    }

    //删除第一次出现的关键字key
    public void remove(int key) {
        //1.原来为空需要考虑,因为第二个循环可能会执行
        if(isEmpty()){//最好直接抛异常
            throw new EmptyException("当前顺序表为空!");//自定义的异常类,此处没有贴代码(很简单)
        }
        //2.正常找到,并且删除
        int i=indexOf(key);
        if(i==-1){
            System.out.println("没有这个数字!");
            return ;
        }
        for (int j = i; j <usedSize-1 ; j++) {
            this.elem[j]=this.elem[j+1];;
        }
        usedSize--;
    }
    // 获取顺序表长度
    public int size() {
        //不需要再进行遍历了,直接返回usedSize即可
        return usedSize;
    }
    // 清空顺序表
    public void clear() {
        this.usedSize=0;
        //如果是引用类型需要一个一个进行置空
    }
}

四、ArrayList介绍及常用方法使用

什么是ArrayList

**定义:**是一个实现了List接口的普通类。

相关的继承关系和实现的接口:继承于AbstractList类,实现了List接口、Cloneable接口、Serializable接口、RandomAcess接口(自然也实现了List,但是这里主要是为了介绍后边的功能铺垫)。

**底层存储:**底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表。

一些拔高点的理解:支持随机访问是因为实现了RandomAcess接口;支持clone是因为实现了Cloneable接口;支持序列化是因为Serializable接口;不是线程安全的,在单线程下可以使用,但在多线程中一般使用Vector或者CopyOnWriteArrayList。

关于ArrayList源码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2o7wr5P-1665031685339)(F:\typora插图\image-20221006112217450.png)]

源码中的函数这么多,我们并不是每个都需要学习和实现的,只需要熟练掌握以下几个就可以了,其他的再复习的时候或者感兴趣的时候可以自行阅读。

常用方法使用注意事项:

1.构造方法&增加函数

构造方法有三种:无参构造【较多】、利用其他接口构建ArrayList、指定顺序表初始化容量——传进去一个长度【较多】

无参构造源码:**当我们调用不带参数的构造方法时,只有第一个add的时候,我们才会分配默认大小为10的内存。**采用的1.5倍扩容(add)。

toString方法可以直接打印,因为Comparable接口重写了相应的方法。

注意:泛型实参不要省略,不然什么样的元素都可以存,那么取得时候非常不方便

public class TestArrayList {
    public static void main(String[] args) {
        // ArrayList创建,推荐写法 
        
        // 方法一:构造一个空的列表 
        List<Integer> list1 = new ArrayList<>();
        
        // 方法二:构造一个具有10个容量的列表
        List<Integer> list2 = new ArrayList<>(10); 
        list2.add(1); 
        list2.add(2);
        list2.add(3);
        //list2.add("hello"); // 编译失败,List<Integer>已经限定了,list2中只能存储整形元素
        
        //方法三: list3构造好之后,与list中的元素一致 
        ArrayList<Integer> list3 = new ArrayList<>(list2);
        //list2存的类型必须是list3存元素的子类   
        
        //  避免省略类型,否则:任意类型的元素都可以存放,使用时将是一场灾难 
        // 禁止!!!! 
        List list4 = new ArrayList(); 
        list4.add("111"); 
        list4.add(100);
        list4.addAll(list3);
    }
}

2.删除组remove/removeAll

remove方法中,参数只要是int类型都被当成了索引,所以如果需要删除指定key值的数字,就需要new对象/装箱。

但是很少这样用,因为我们操作对象一般是引用类型,也就不存在这样的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OOZUhwtm-1665031685340)(F:\typora插图\image-20221003170834180.png)]

3.子表subList

本质上并不是拷贝出来了,而是拿到了对应范围的地址。所以对sublist更改也会改变原来的值

左闭右开的形式,返回值是List类型的

4.add方法

通过源码我们可以发现,其实有一个默认变量存储的是10,但其实一开始创建的数组大小是0,那么这时又怎么能够向里边元素呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z5JGGPdT-1665031685341)(F:\typora插图\image-20221006120247615.png)]

简而言之,就是首次add时,进行了扩容。

【其他的暂时没有发现,以后可能会进行补充。】

ArrayList的遍历:

4种方法:

  • foreach
  • fori
  • Iterator 【迭代器方法】
  • sout【Comparable重写了toString方法,List继承了它,ArrayList实现了这个类,println可以直接打印】【简单,没有演示】
public static void main(String[] args) {
    ArrayList<Integer>arrayList=new ArrayList<>();
    arrayList.add(1);
    arrayList.add(12);
    arrayList.add(13);
    arrayList.add(14);
    arrayList.add(15);
    arrayList.add(16);
    int size=arrayList.size();
    for (int x:arrayList) {
        System.out.print (x+" ");
    }
    System.out.println();
}
public static void main1(String[] args) {
    ArrayList<Integer>arrayList=new ArrayList<>();
    arrayList.add(1);
    arrayList.add(12);
    arrayList.add(13);
    arrayList.add(14);
    arrayList.add(15);
    arrayList.add(16);
    int size=arrayList.size();
    for (int i = 0; i < size; i++) {
        System.out.print (arrayList.get(i)+" ");
    }
    System.out.println();
}
public static void main2(String[] args) {
    ArrayList<Integer>arrayList=new ArrayList<>();
    arrayList.add(1);
    arrayList.add(12);
    arrayList.add(13);
    arrayList.add(14);
    arrayList.add(15);
    arrayList.add(16);
    int size=arrayList.size();
    Iterator<Integer> it=arrayList.iterator();
    while(it.hasNext()){//一定是实现了Iteralbe接口才可以
        System.out.print(it.next()+" ");
        //打印它的同时,it往后走一步
    }
    System.out.println();
}

关于迭代器

Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayListHashSet 等集合。

迭代器 it 的基本操作是 next 、hasNext 和 remove。

调用 it.next() 会返回迭代器的下一个元素,并且更新迭代器的状态。

调用 it.hasNext() 用于检测集合中是否还有元素。

调用 it.remove() 将迭代器返回的元素删除。

具体介绍:Java迭代器介绍

五、ArrayList扩容机制详解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YqhvZE3P-1665031685341)(F:\typora插图\image-20221006115152061.png)]

扩容的步骤

  1. 检查是不是真正需要扩容,如果是准备用grow函数扩容

  2. 预估需要库容的大小

    正常1.5倍,如果超过1.5倍那么按照用户所需大小扩容

    真正扩容前进行检测是不是能扩容成功,防止太大导致扩容失败

  3. 使用copyOf开始工作了

通过看扩容的源码,我们可以发现,它是会默认扩容为1.5倍大小的,优点是可以完成功能,缺点是在我们只再需要一个的时候,它反而扩了1.5倍大小,这时其实是有点浪费空间的。

六、关于链表

**定义:链表是一种物理存储结构上非连续存储结构,**数据元素的逻辑顺序是通过链表的引用链接起来的。

**要点:**链式结构在逻辑上是连续的,但是物理结构不一定连续。

为什么是不一定,而不是不?

现实中的节点一般都是从堆上申请出来的,从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间有可能是连续的,也可能不随时连续的。

链表的分类:

  • 按照方向的个数:单向、双向

  • 按照循不循环分:循环、非循环

  • 按照带不带头分:带头节点、不带头节点

他们进行排列组合,也就是2^3=8种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZGKs5iV-1665031685342)(F:\typora插图\image-20221003175656365.png)]

关于带头不带头

带不带头只是方便了进行增删查改的工作,所以我们在画图的时候就不再单独画出这种情况,所以这时我们就看4种情况下的图示即可。

为什么带头结点的比较简单呢?这是因为,带头的话,我们不需要再单独考虑头结点增删改的情况,特殊情况的分析又少了一种,会更加方便。

关于链表重点

  • 无头的单向的非循环链表:笔试高频,但是一般不会单独用来存储数据,实际上更多的是作为其他数据结构的子结构。
  • 无头双向链表:java的集合框架库LinkedList底层实现就是无头双向链表。

关于LinkedList的三种使用方法:

  • 1.作为双向链表使用
  • 2.作为队列的对象
  • 3.作为双端队列使用

接下来,我们也简单实现一下单链表的功能

//无头单向非循环链表实现
public class MySingleLinkedList {
    //节点和构造方法
    class ListNode{
        int val;
        ListNode next;
        public ListNode(int val){
            this.val=val;
        }
        public ListNode(){
            ;
        }
    }
    public ListNode head;//一个引用,具有实际意义
    //头插
    public void addFirst(int data){
        ListNode node=new ListNode(data);
        node.next=this.head;
        head=node;
    }
    //尾插法
    public void addLast(int data){
        ListNode node=new ListNode(data);
        if(head==null){
            head=node;
            return ;//一定记得结束这个过程
        }
        ListNode cur=this.head;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=node;
    }
    //任意位置插入,第一个数据节点为0号下标
    public boolean addIndex(int index,int data){
        if(index<0||index>size()){
            System.out.println("位置坐标不合法!");
            return false;
        }
        if(index==0){
            addFirst(data);
            return true;
        }
        if(index==size()){
            addLast(data);
            return true;
        }
        ListNode node=new ListNode(data);
        if(head==null){
            head=node;
        }
        ListNode prev=this.head;
        ListNode cur=head.next;
        while(index-1!=0){
            prev=cur;//先从前向后绑定
            cur=cur.next;
            index--;
        }
        node.next=cur;
        prev.next=node;
        return true;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur=this.head;
        while(cur!=null){
            if(cur.val==key){
                return true;
            }
            cur=cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        if(head==null){
            return ;
        }
        if(head.val==key){
            head=head.next;
            return ;
        }
        ListNode cur=this.head;
        while(cur.next!=null) {
            if (cur.next.val == key) {
                cur.next = cur.next.next;
                return;
            } else {
                cur = cur.next;
            }
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
        if(head==null){
            return ;
        }
        if(head.next==null){
            if(head.next.val==key){
                head.next=null;
            }
            return ;
        }
        ListNode cur=this.head.next;
        ListNode prev=head;
        while(cur!=null){
            if(cur.val==key){
                prev.next=cur.next;
                cur=cur.next;
            }else{
                prev=cur;
                cur=cur.next;
            }
        }
        if(head.val==key){
            head=head.next;
        }
    }
    //得到单链表的长度
    public int size(){
        int count=0;
        ListNode cur=this.head;
        while(cur!=null){
            count++;
            cur=cur.next;
        }
        return count;
    }
    public void display(){
        ListNode cur=this.head;
        while(cur!=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
        System.out.println();
    }
    public void clear(){
        //引用类型需要单独置空
        ListNode cur=this.head;
        while(cur!=null){
            ListNode curNext=cur.next;
            cur=null;
            cur=curNext;
        }
    }
}

七、LinkedList介绍以及常用方法使用

同ArrayList,LinkedList也是一个实现了List接口的类.顶层是一个无头双向链表

Java中LinkedList的使用方法

不需要刻意的去记忆,可以在碰到题目的时候去查。

八、ArrayList与LinkedList对比

这也算是一个高频的面试题。

一般有以及几种问法

  • 数组和链表的区别是什么?
  • 顺序表和链表的区别是什么?
  • ArrayList和LinkedList区别是什么?
不同点ArrayListLinkedLst
内存上逻辑物理存储连续逻辑连续,空间不一定
随机存储支持,通过下标索引不支持
头增头删需要移动元素,时间复杂度为O(N)只需要修改有限指向,时间复杂度为O(1)
扩容/容量问题容量不够时需要扩容没有容量概念
应用场景元素需要频繁随机访问元素频繁进行插入删除操作

上边是我们的逻辑框架,我们在组织语言的时候,可以从共性出发。

例如:

首先,在增删查改方面,查找元素或者改变某个元素的值的时候,顺序表支持随机访问,时间复杂度为O(1),而链表必须遍历整个链表,时间复杂度为O(N);在头插入元素和头删除元素时,顺序表必须移动元素,时间复杂度为O(N),链表只需要改变有限个指向,时间复杂度为O(1);其次,在存储方面,顺序表使用数组实现的,逻辑和物理上都是连续的,而链表是散落的节点,通过引用链接到一起的,逻辑上连续,但是物理不一定连续。

所以,我们一般顺序表应用于需要频繁随机访问和高效存储的场景下,而链表应用于元素需要频繁进行插入删除操作的场景下。

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值