Java数据结构:从入门到精通(五)

ArrayList与顺序表

1. 线性表

线性表(linear list)是由n个具有相同特性的数据元素组成的有限序列。作为一种实际应用中广泛使用的数据结构,常见的线性表包括:顺序表、链表、栈、队列等。

在逻辑结构上,线性表呈现为连续的线性结构,即一条直线排列。然而在物理存储结构上,元素并不一定连续存储。线性表通常采用两种存储方式:数组(顺序存储)和链式结构(链式存储)。

2. 顺序表

顺序表是一种采用数组存储的线性结构,其特点是数据元素在物理地址上连续存储。通过数组实现顺序表,可以高效地进行数据的增删查改操作。

2.1 接⼝的实现

框架:

java 复制代码
public class SeqList {
private int[] array;
private int size;
// 默认构造⽅法
SeqList(){ }
// 将顺序表的底层容量设置为initcapacity
SeqList(int initcapacity){ }
// 新增元素,默认在数组最后新增
public void add(int data) { }
// 在 pos 位置新增元素
public void add(int pos, int data) { }
// 判定是否包含某个元素
public boolean contains(int toFind) { return true; }
// 查找某个元素对应的位置
public int indexOf(int toFind) { return -1; }
// 获取 pos 位置的元素
public int get(int pos) { return -1; }
// 给 pos 位置的元素设为 value
public void set(int pos, int value) { }
//删除第⼀次出现的关键字key
public void remove(int toRemove) { }
// 获取顺序表⻓度
public int size() { return 0; }
// 清空顺序表
public void clear() { }
// 打印顺序表,注意:该⽅法并不是顺序表中的⽅法,为了⽅便看测试结果给出的
public void display() { }
}

参考代码:

java 复制代码
package com.demo1;

public class SeqList {
    private int[] array;
    private int size;
    // 默认构造⽅法
    SeqList(){
        size = 0;
        array = new int[5];
    }
    // 将顺序表的底层容量设置为initcapacity
    SeqList(int initcapacity){
        array = new int[initcapacity];
        size = 0;
    }
    public boolean isFull(){
        if(size == array.length){
            return true;
        }
        return false;
    }
    public boolean isEmpty(){
        if(size == 0){
            return true;
        }
        return false;
    }
    // 新增元素,默认在数组最后新增
    public void add(int data) {
        if(!isFull()) {
            array[size] = data;
            size++;
        }
    }
    public void moveBack(int start,int end){
        for(int i = end;i >= start; i--){
            array[i+1] = array[i];
        }
    }
    public void moveFront(int start,int end){
        for(int i = start; i <= end; i++){
            array[i - 1] = array[i];
        }
    }
    // 在 pos 位置新增元素
    public boolean add(int pos, int data) {
        if(!isFull()){
            if(pos > size || pos < 0){
                System.out.println("插入位置不对,请重新选择插入位置");
                return false;
            }else if(pos == size) {
               array[pos] = data;
               size++;
            }else {
                moveBack(pos, size - 1);
                array[pos] = data;
                size++;
            }
        }
        return true;
    }

    // 判定是否包含某个元素
    public boolean contains(int toFind) {
        if(!isEmpty()) {
            for (int i = 0; i < size; i++) {
                if (array[i] == toFind) {
                    return true;
                }
            }
            return false;
        }
        return false;
    }
    // 查找某个元素对应的位置
    public int indexOf(int toFind) {
        if(!isEmpty()) {
            for (int i = 0; i < size; i++) {
                if (array[i] == toFind) {
                    return i;
                }
            }
        }
        return -1;
    }
    // 获取 pos 位置的元素
    public int get(int pos) {
        if(!isEmpty()) {
            if (pos < 0 || pos > size - 1) {
                System.out.println("该位置没有元素");
                return -1;
            } else {
                return array[pos];
            }
        }
        return -1;
    }
    // 给 pos 位置的元素设为 value
    public boolean set(int pos, int value) {
        if(!isEmpty()) {
            if (pos < 0 || pos >= size) {
                return false;
            } else {
                array[pos] = value;
                return true;
            }
        }
        return false;
    }
    //删除第⼀次出现的关键字key
    public boolean remove(int toRemove) {
        if(!isEmpty()){
            for(int i = 0; i < size; i++){
                if(array[i] == toRemove){
                    if(i == size-1){
                        size--;
                        return true;
                    }else{
                        moveFront(i+1,size-1);
                        size--;
                        return true;
                    }
                }
            }
        }
        return false;
    }
    // 获取顺序表⻓度
    public int size() {
        return size;
    }
    // 清空顺序表
    public void clear() {
        size = 0;
    }
    // 打印顺序表,注意:该⽅法并不是顺序表中的⽅法,为了⽅便看测试结果给出的
    public void display() {
        for(int i = 0; i < size; i++){
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }
}

3. ArrayList简介

在集合框架中,ArrayList是⼀个普通的类,实现了List接⼝,具体框架图如下:

【说明】

  1. ArrayList采用泛型实现,使用时需先进行实例化
  2. ArrayList实现了RandomAccess接口,表明其支持随机访问特性
  3. ArrayList实现了Cloneable接口,具备克隆能力
  4. ArrayList实现了Serializable接口,支持序列化操作
  5. 与Vector不同,ArrayList非线程安全,适用于单线程环境;多线程场景可选用Vector或CopyOnWriteArrayList
  6. ArrayList底层基于连续存储空间实现,具有动态扩容特性,属于动态顺序表结构

4. ArrayList使⽤

4.1 ArrayList的构造

方法 说明
ArrayList() 创建一个空的 ArrayList
ArrayList(Collection<? extends E> c) 通过现有集合创建 ArrayList
ArrayList(int initialCapacity) 创建具有指定初始容量的 ArrayList
java 复制代码
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);
        // 避免省略类型,否则:任意类型的元素都可以存放,使⽤时将是⼀场灾难
        List list4 = new ArrayList();
        list4.add("111");
        list4.add(100);
    }

4.2 ArrayList常⻅操作

ArrayList虽然提供的⽅法⽐较多,但是常⽤⽅法如下所⽰,需要⽤到其他⽅法时,同学们⾃⾏查看

ArrayList的帮助⽂档。

方法 说明
boolean add(E e) 在列表末尾添加元素 e
void add(int index, E element) 在指定索引位置插入元素 e
boolean addAll(Collection<? extends E> c) 在列表末尾添加集合 c 中的所有元素
E remove(int index) 删除并返回指定索引位置的元素
boolean remove(Object o) 删除列表中首次出现的元素 o
E get(int index) 获取指定索引位置的元素
E set(int index, E element) 将指定索引位置的元素替换为 e
void clear() 清空列表中的所有元素
boolean contains(Object o) 判断列表中是否包含元素 o
int indexOf(Object o) 返回元素 o 首次出现的索引
int lastIndexOf(Object o) 返回元素 o 最后一次出现的索引
List<E> subList(int fromIndex, int toIndex) 获取指定索引范围内的子列表
java 复制代码
public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("JavaSE");
        list.add("JavaWeb");
        list.add("JavaEE");
        list.add("JVM");
        list.add("测试课程");
        System.out.println(list);
        // 获取list中有效元素个数
        System.out.println(list.size());
        // 获取和设置index位置上的元素,注意index必须介于[0, size)间
        System.out.println(list.get(1));
        list.set(1, "JavaWEB");
        System.out.println(list.get(1));
        // 在list的index位置插⼊指定元素,index及后续的元素统⼀往后搬移⼀个位置
        list.add(1, "Java数据结构");
        System.out.println(list);
        // 删除指定元素,找到了就删除,该元素之后的元素统⼀往前搬移⼀个位置
        list.remove("JVM");
        System.out.println(list);
        // 删除list中index位置上的元素,注意index不要超过list中有效元素个数,否则会抛出下标越界异常
        list.remove(list.size()-1);
        System.out.println(list);
        // 检测list中是否包含指定元素,包含返回true,否则返回false
        if(list.contains("测试课程")){
            list.add("测试课程");
        }
        // 查找指定元素第⼀次出现的位置:indexOf从前往后找,lastIndexOf从后往前找
        list.add("JavaSE");
        System.out.println(list.indexOf("JavaSE"));
        System.out.println(list.lastIndexOf("JavaSE"));
        // 使⽤list中[0, 4)之间的元素构成⼀个新的SubList返回,但是和ArrayList共⽤⼀个elementData数组
        List<String> ret = list.subList(0, 4);
        System.out.println(ret);
        list.clear();
        System.out.println(list.size());
    }

4.3 ArrayList的遍历

ArrayList可以通过三种方式遍历:

  1. for循环配合下标访问
  2. 增强for循环(foreach)
  3. 使用迭代器(Iterator)
java 复制代码
public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(3);
        list.add(5);
        list.add(7);
        list.add(9);
        //使用下标+for遍历
        for(int i = 0; i < list.size(); i++){
            System.out.print(list.get(i) + " ");
        }
        System.out.println();
        //借助foreach遍历
        for(Integer integer : list){
            System.out.print(integer + " ");
        }
        System.out.println();
        Iterator<Integer> it = list.listIterator();
        while(it.hasNext()){
            System.out.print(it.next() + " ");
        }
        System.out.println();
    }

注意:

  1. ArrayList最常用的遍历方式有两种:通过for循环结合下标访问,以及使用foreach语法。

  2. 迭代器是设计模式的一种,后续在大家接触更多容器类型时,我们会进一步讲解这个概念。

4.4 ArrayList的扩容机制

下⾯代码有缺陷吗?为什么?

java 复制代码
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        list.add(i);
       }
}

ArrayList是⼀个动态类型的顺序表,即:在插⼊元素的过程中会⾃动扩容。以下是ArrayList源码

(JDK17)中扩容⽅式:

java 复制代码
   public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length) //第⼀次存储的时候 elementData.length=0,s = 0
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    private Object[] grow() {
        return grow(size + 1);
    }
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) {
            int newCapacity = ArraysSupport. newLength (oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1 /* preferred growth */ );
            return elementData = Arrays. copyOf (elementData, newCapacity);
        } else {
            return elementData = new Object[Math. max ( DEFAULT_CAPACITY ,
                    minCapacity)];
        }
    }
//oldLength: 当前数组的⻓度
//minGrowth: 最⼩需要增加的⻓度
//prefGrowth: ⾸选的增⻓⻓度(通常是当前⼤⼩的⼀半)
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
//当前⻓度加上 minGrowth 和 prefGrowth 中的较⼤值。
        int prefLength = oldLength + Math. max (minGrowth, prefGrowth); // might
        overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH ) {
            return prefLength;
        } else {
// put code cold in a separate method
            return hugeLength (oldLength, minGrowth);
        }
    }
    private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError(
                    "Required array length " + oldLength + " + " + minGrowth + " is
                    too large");
        ; else if (minLength <= SOFT_MAX_ARRAY_LENGTH ) {
            return SOFT_MAX_ARRAY_LENGTH ;
        } else {
            return minLength;
        }
    }
  • 如果调⽤不带参数的构造⽅法,第⼀次分配数组⼤⼩为10
  • 后续扩容为1.5倍扩容

5. ArrayList的具体使⽤

5.1 简单的洗牌算法

java 复制代码
package com.demo2;

public class Card {
    public String rank;//牌面值
    public String suit;//花色

    @Override
    public String toString() {
        return String.format("[%s %s]",suit,rank);
    }
}
java 复制代码
package com.demo2;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class CardDemo {
    public static final String[] SUITS = {"♠", "♥", "♣", "♦"};
    public static final String[] RANK = {"3","4","5","6","7","8","9","10",
            "J","Q","K","A","2"};
    //买一副牌
    private static List<Card> buyDeck(){
        List<Card> deck = new ArrayList<>(52);
        for(int i = 0; i < 4; i++){
            for(int j = 0; j < 13; j++){
                String suit = SUITS[i];
                String rank = RANK[j];
                Card card = new Card();
                card.rank = rank;
                card.suit = suit;
                deck.add(card);
            }
        }
        return deck;
    }
    private static void swap(List<Card> deck, int i, int j){
        Card t = deck.get(i);
        deck.set(i,deck.get(j));
        deck.set(j,t);
    }
    private static void shuffle(List<Card> deck){
        Random random = new Random(20190905);
        for(int i = deck.size() - 1; i > 0; i--){
            int r = random.nextInt(i);
            swap(deck,i,r);
        }
    }

    public static void main(String[] args) {
        List<Card> deck = buyDeck();
        System.out.println("刚买回来的牌:");
        System.out.println(deck);
        shuffle(deck);
        System.out.println("洗过的牌");
        System.out.println(deck);
        //三个人,每人轮流抓5张牌
        List<List<Card>> hands = new ArrayList<>();
        hands.add(new ArrayList<>());
        hands.add(new ArrayList<>());
        hands.add(new ArrayList<>());
        for(int i = 0; i < 5; i++){
            for(int j = 0; j < 3; j++){
                hands.get(j).add(deck.remove(0));
            }
        }
        System.out.println("剩余的牌:");
        System.out.println(deck);
        System.out.println("玩家A手中的牌:");
        System.out.println(hands.get(0));
        System.out.println("玩家B手中的牌:");
        System.out.println(hands.get(1));
        System.out.println("玩家C手中的牌:");
        System.out.println(hands.get(2));
    }

}

5.2 杨辉三⻆

118. 杨辉三角 - 力扣(LeetCode)

6. ArrayList的问题及思考

  1. ArrayList底层采用连续存储空间,在任意位置插入或删除元素时,需要移动后续所有元素,导致时间复杂度为O(N)。

  2. 扩容操作涉及申请新内存、数据迁移和释放旧空间,会产生较大的性能开销。

  3. 扩容通常采用2倍增长策略,可能导致空间浪费。例如当前容量100,扩容到200后仅插入5个元素,就会造成95个空间的浪费。

思考:如何优化上述问题?

感谢您的观看!

相关推荐
多米Domi01136 分钟前
0x3f 第48天 面向实习的八股背诵第五天 + 堆一题 背了JUC的题,java.util.Concurrency
开发语言·数据结构·python·算法·leetcode·面试
故以往之不谏1 小时前
函数--值传递
开发语言·数据结构·c++·算法·学习方法
向哆哆2 小时前
构建跨端健身俱乐部管理系统:Flutter × OpenHarmony 的数据结构与设计解析
数据结构·flutter·鸿蒙·openharmony·开源鸿蒙
独自破碎E3 小时前
【总和拆分 + 双变量遍历】LCR_012_寻找数组的中心下标
数据结构·算法
txzrxz3 小时前
结构体排序,双指针,单调栈
数据结构·算法·双指针算法·单调栈·结构体排序
wWYy.3 小时前
算法:二叉树最大路径和
数据结构·算法
一条大祥脚4 小时前
ABC357 基环树dp|懒标记线段树
数据结构·算法·图论
苦藤新鸡4 小时前
50.腐烂的橘子
数据结构·算法
无限进步_4 小时前
面试题 02.02. 返回倒数第 k 个节点 - 题解与详细分析
c语言·开发语言·数据结构·git·链表·github·visual studio
Hello World . .4 小时前
数据结构:栈和队列
c语言·开发语言·数据结构·vim