当前位置: 首页 > article >正文

【数据结构】(5) ArrayList 顺序表

一、使用 ArrayList

        ArrayList 就是数组的封装,但是数组只有 [] 操作存取值,和 .length 操作获取数组内存长度;而 ArrayList 有更多的功能:

1、创建对象

2、扩容机制

        ArrayList 有自动扩容机制,在插入元素时不用担心数组容量是否足够。创建对象时,数组容量默认为为 10,容量不够时,每次扩容都会乘以 1.5 倍。源码如下:

        调用无参构造函数,数组初始化为空数组:

        第一次添加一个元素:

        进入 calculateCapacity 计算需用数组长度:

        计算得到需用数组长度为 10:

        此时,10 > 0,数组容量不够,需要扩容:

        若添加元素时,触发扩容机制(即 Arrays.copyOf ),过程如下:

  • 创建一个新的数组,长度比原来大。
  • 将旧数组的元素拷贝到新数组中。
  • 添加新的元素到新数组中。
  • 释放旧数组的空间。

        可以看到扩容存在拷贝操作,需要遍历旧数组中的元素,因此效率低下。故创建对象时应设置一个合适的大小避免频繁扩容

        从上面的源代码可以看到,Arrays.copyOf 方法,创建了 newCapacity 容量的新数组,并把旧数组 elementData 复制到新数组中,并返回新数组。

3、插入一个元素

.addAll(list):把 list 中的所有元素添加到末尾。

        总结一下获取数组长度的方法:

  • 数组为 .length
  • String 为 .length()
  • 集合类 为 .size()

        同样的作用却有不同的名字,这是在以后开放中需要避免的,我们应该统一风格。

4、获取、设置一个元素

5、遍历元素

        使用 for 循环或 for-each,重点讲 for-each。回顾集合类框架,集合接口 Collection 继承了可迭代的接口 Iterable

        因此实现了 Iterable 接口的 iterator 方法的集合类,可通过对象调用 iterator 方法得到一个迭代器。ArrayList 实现 iterator 方法的源码如下:

        手动实现遍历:

        迭代器的效果等价于 for-each,因此实现了 Iterable 接口的 iterator 方法的类,都是可迭代的,即可使用 for-each 语句的

6、删除一个元素

7、获取一个元素的位置

8、判断是否包含一个元素

        源码使用了 indexOf:

9、获取子列表

        若想避免子列表牵动父列表,应创建新的列表:

10、清空元素

        Java 中存在垃圾回收机制,JVM会自动回收没被引用的内存空间,不需要手动地释放元素所占空间(如 C 中的 free)。

二、ArrayList 的使用练习

1、实现简单的洗牌算法

        Card 类,对象表示一张牌,存有花色和牌值:

class Card {
    String suit; // 花色
    String value; // 牌值

    public Card(String suit, String value) {
        this.suit = suit;
        this.value = value;
    }

    public String getSuit() {
        return suit;
    }

    public String getValue() {
        return value;
    }

    public void setSuit(String suit) {
        this.suit = suit;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return this.suit + this.value;
    }
}

        主类中 createPokerCards 方法,创建一副新牌:

    public static ArrayList<Card> createPokerCards() {
        ArrayList<Card> cards = new ArrayList<Card>();
        String[] suits = {"♠", "♥", "♦", "♣"};
        String[] values = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        for (String suit : suits) {
            for (String value : values) {
                cards.add(new Card(suit, value));
            }
        }
        return cards;
    }

         主类中 shufflePokerCards 方法,打乱扑克牌:

    public static void shufflePokerCards(ArrayList<Card> cards) {
        Random random = new Random();
        for (int i = 0; i < cards.size(); i++) {
            int j = random.nextInt(cards.size()); // 随机生成 j 位置
            // 交换 i 位置和 j 位置的牌
            Card temp = new Card(cards.get(i).getSuit(), cards.get(i).getValue()); // 创建新的对象,避免后续 temp 因 i 位置被 j 处覆盖而发生改变
            cards.set(i, cards.get(j));
            cards.set(j, temp);
        }
    }

        主类中 dealPokerCards 方法,发牌:

    public static ArrayList<ArrayList<Card>> dealPokerCards(ArrayList<Card> cards, int numPlayers) {
        ArrayList<ArrayList<Card>> hands = new ArrayList<>();
        // 创建 numPlayers 手空牌
        for (int i = 0; i < numPlayers; i++) {
            ArrayList<Card> hand = new ArrayList<>();
            hands.add(hand);
        }
        int numCards = cards.size() / numPlayers; // 每人手牌数
        // 给每人发牌
        for (int i = 0; i < numCards; i++) {
            for (int j = 0; j < numPlayers; j++) {
                Card card = cards.remove(0); // 取出第一张牌
                hands.get(j).add(card); // 给 j 号玩家发牌
            }
        }
        return hands;
    }

        主类中 printHands 方法,打印所有手牌:

    public static void printHands(ArrayList<ArrayList<Card>> hands) {
        for (ArrayList<Card> hand : hands) {
            System.out.println("手牌:" + hand);
        }
    }

        主逻辑:

    public static void main(String[] args) {
        // 创建扑克牌
        ArrayList<Card> cards = createPokerCards();
        System.out.println("新牌:" + cards);
        // 洗牌
        shufflePokerCards(cards);
        System.out.println("洗牌:" + cards);
        // 发牌
        ArrayList<ArrayList<Card>> hands = dealPokerCards(cards, 4);
        // 打印手牌
        printHands(hands);
    }

        结果:

2、打印杨辉三角

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

分析:

  • 第 i (0 ~ numRows) 行有 i + 1 列。
  • 每一行第一个和最后一个都为 1。
  • 中间的第 j 个等于 [i-1][j-1] + [i-1][j] 。
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> rows = new ArrayList<>(numRows);
        for(int i = 0; i < numRows; i++) {
            List<Integer> row = new ArrayList<>(i + 1);
            for(int j = 0; j < i+1; j++) {
                if(j == 0 || j == i) {
                    row.add(1);
                }
                else {
                    List<Integer> lastRow = rows.get(i - 1);
                    row.add(lastRow.get(j - 1) + lastRow.get(j));
                }
            }
            rows.add(row);
        }
        return rows;
    }
}

三、自己实现一个 MyArrayList

1、功能实现

import java.util.Arrays;

/** 功能代码 **/
public class MyArrayList {
    private int[] arr;
    private int size = 0; // 已用容量

    // 构造函数, 默认容量为10
    public MyArrayList() {
        arr = new int[10];
    }

    // 构造函数, 指定容量
    public MyArrayList(int capacity) {
        arr = new int[capacity];
    }

    // 获取元素个数
    public int size() {
        return size;
    }

    // 扩容, 私有, 不需要暴露给外界
    private void resize() {
        // 创建一个新的数组, 容量为原来的1.5倍
        int oldCapacity = arr.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为原来的1.5倍; cpu 计算加减移位比乘除快
        // 创建新数组,并将原数组中的元素复制到新数组中
        arr = Arrays.copyOf(arr, newCapacity);
    }

    // 添加元素
    public void add(int val) {
        // 已满,扩容
        if (size == arr.length) {
            resize();
        }
        // 添加元素
        arr[size++] = val;
    }

    // 指定位置添加元素
    public void add(int index, int val) {
        // 超出范围
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        // 已满,扩容
        if (size == arr.length) {
            resize();
        }
        // 移动元素
        for (int i = size - 1; i >= index; i--) {
            arr[i + 1] = arr[i];
        }
        // 添加元素
        arr[index] = val;
        size++;
    }

    // 根据下标,获取元素
    public int get(int index) {
        // 超出范围
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        return arr[index];
    }

    // 根据下标,设置元素
    public void set(int index, int val) {
        // 超出范围
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        arr[index] = val;
    }

    // 根据下标,删除元素,返回删除的元素
    public int remove(int index) {
        // 超出范围
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        int val = arr[index];
        // 移动元素
        for (int i = index; i < size - 1; i++) {
            arr[i] = arr[i + 1];
        }
        size--;
        return val;
    }

    // 根据值,删除元素
    public boolean delete(int val) {
        for (int i = 0; i < size; i++) {
            if (arr[i] == val) {
                remove(i);
                return true;
            }
        }
        return false;
    }

    // 查找元素所在下标,不存在返回-1
    public int indexOf(int val) {
        for (int i = 0; i < size; i++) {
            if (arr[i] == val) {
                return i;
            }
        }
        return -1;
    }

    // 倒着查找元素所在下标,不存在返回-1
    public int lastIndexOf(int val) {
        for (int i = size - 1; i >= 0; i--) {
            if (arr[i] == val) {
                return i;
            }
        }
        return -1;
    }

    // 判断是否包含元素
    public boolean contains(int val) {
        return indexOf(val) >= 0;
    }

    // 清空元素
    public void clear() {
        size = 0;
    }

    // 打印元素
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(arr[i]);
            if (i < size - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

2、单元测试

/** 单元测试 **/
class MyArrayListTest {
    // 测试1:构造函数和 add 方法 和 size 方法 和 toString 方法
    public static void test1() {
        MyArrayList list = new MyArrayList(3);
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(1,4); // 自动扩容

        if (list.size() == 4) {
            System.out.println("test1 pass");
        }
        else {
            System.out.println("test1 fail");
        }

        if (list.toString().equals("[1, 4, 2, 3]")) {
            System.out.println("test1 pass");
        }
        else {
            System.out.println("test1 fail");
        }

        list.add(100,5); // 越界
    }

    // 测试2:get 方法 和 set 方法
    public static void test2() {
        MyArrayList list = new MyArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        if (list.get(0) == 1 && list.get(1) == 2 && list.get(2) == 3) {
            System.out.println("test2 pass");
        }
        else {
            System.out.println("test2 fail");
        }
        list.set(1, 100);
        if (list.get(1) == 100) {
            System.out.println("test2 pass");
        }
        else {
            System.out.println("test2 fail");
        }

        list.get(100); // 越界
        list.set(100, 100); // 越界
    }

    // 测试3:remove 方法 和 delete 方法
    public static void test3() {
        MyArrayList list = new MyArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.remove(2);

        if (list.toString().equals("[1, 2, 4, 5]")) {
            System.out.println("test3 pass");
        }
        else {
            System.out.println("test3 fail");
        }

        list.delete(4);
        if (list.toString().equals("[1, 2, 5]")) {
            System.out.println("test3 pass");
        }
        else {
            System.out.println("test3 fail");
        }

        if (!list.delete(100)) {
            System.out.println("test3 pass");
        }
        else {
            System.out.println("test3 fail");
        }

        list.remove(100); // 越界
    }

    // 测试4:indexOf 方法 和 lastIndexOf 方法 和 contains 方法 和 clear 方法
    public static void test4() {
        MyArrayList list = new MyArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(2);

        if (list.indexOf(2) == 1 && list.lastIndexOf(2) == 3 && list.contains(2)) {
            System.out.println("test4 pass");
        }
        else {
            System.out.println("test4 fail");
        }

        if (list.indexOf(100) == -1 && list.lastIndexOf(100) == -1 && !list.contains(100)) {
            System.out.println("test4 pass");
        }
        else {
            System.out.println("test4 fail");
        }

        list.clear();
        if (list.size() == 0) {
            System.out.println("test4 pass");
        }
        else {
            System.out.println("test4 fail");
        }
    }

    public static void main(String[] args) {
//        test1();
//        test2();
//        test3();
        test4();
    }
}

3、注意事项

  • 顺序表是一个有序表,在实现添加一个元素时,不能直接将 index 处的元素移到最后面,再把新元素添加到 index 处;这样虽然避免了大量移动,但是打乱了顺序表的有序性,不符合要求。
  • 顺序表的缺点:顺序表在中间插入和删除需要大量移动,时间复杂度为 O(N);扩容也需要大量拷贝,降低效率;很有可能造成内存空间浪费。
  • 后续的链表可以解决以上问题,但同样带来了其它的问题,总的来说还是 ArrayList 更常用

http://www.kler.cn/a/535891.html

相关文章:

  • DKG(Distributed Key Generation)协议
  • 深入理解 YUV Planar 和色度二次采样 —— 视频处理的核心技术
  • 深度整理总结MySQL——SQL的执行顺序和流程
  • NSS-DAY2
  • 语言月赛 202311【基因】题解(AC)
  • 什么叫DeepSeek-V3,以及与GPT-4o的区别
  • 确保数据一致性:RabbitMQ 消息传递中的丢失与重复问题详解
  • 如何查看:Buildroot所使用Linux的版本号、gcc交叉编译工具所使用的Linux的版本号、开发板上运行的Linux系统的版本号
  • 使用外骨骼灵活远程控制协作机器人案例
  • 如何利用Java爬虫获取商品销量详情实战指南
  • Spring Boot 自动装配机制深度解析
  • VUE之组件通信(二)
  • Git 分支管理策略与实践
  • 怎麼在Chrome中設置代理伺服器?
  • MySQL 进阶专题:索引(索引原理/操作/优缺点/B+树)
  • 责任链模式(Chain Responsibility)
  • 深度学习里面的而优化函数 Adam,SGD,动量法,AdaGrad 等 | PyTorch 深度学习实战
  • HbuilderX中,实现Gzip的两种方法
  • 【数据结构-Trie树】力扣720. 词典中最长的单词
  • android 打包AAR-引入资源layout-安卓封包
  • 网络计算机的五个组成部分
  • 2.5-数据结构:AVL树
  • DeepSeek 开源模型全解析(2024.1.1–2025.2.6)
  • 2025年2月6日(anaconda cuda 学习 基本命令)
  • 《ISO/SAE 21434-2021 道路汽车--网络安全工程》标准解读
  • 大模型的底层逻辑及Transformer架构