数据结构基础01:动态数组

数据结构的分类

线性结构

动态数组、栈、队列、链表、哈希表等

树结构

二叉树、二分搜索树、AVL、红黑树、堆、B类树、哈夫曼树等

图结构

邻接矩阵、领接表

静态数组

数组最大的优点是可以根据索引快速查询,索引最好有实际的语意,否则最好使用别的数据结构

Java自带数组的索引没有语意,且长度不可变,称为静态数组

因此,可以基于静态数组拓展一些功能,一般为增、删、改、查

public class Algorithm {

    public static void main(String[] args) {

        Array<String> arr = new Array<>(20);

        for (int i = 0; i < 10; i++) {
            arr.addLast(String.format("第%d个元素", i));
        }

        System.out.println(arr);

        Array<Student> stu = new Array<>(20);

        stu.addLast(new Student("xiaoming", 100));
        stu.addLast(new Student("xiaohong", 80));
        stu.addLast(new Student("xiaoming", 100));

        System.out.println(stu);
    }
}

/**
 * 基于静态数组,拓展增删改查功能
 */
class Array<E> {

    /**
     * 底层采用Java的数据类型
     * 加上泛型,也可用于自定义类
     */
    private E[] data;
    private int size;

    public Array(int capacity){

        /**
         * 泛型具有擦除机制,在运行时Java只知道存储的对象是一个Object
         * 而对于Java的数组来说,必须先知道它持有的对象的具体类型,而泛型的这种运行时擦除机制违反了数组安全检查的原则
         * 因此必须使用泛型声明一个数组,但不能直接用泛型来建立数组的实例,如data = new E[capacity]是错的
         * 先实例化一个Object类型的数组,然后强制转换为E[]
         */
        data = (E[]) new Object[capacity];
        size = 0;
    }

    /**
     * 不指定数组容量时,默认为10
     * this()指代的就是构造函数
     */
    public Array(){

        this(10);
    }

    /**
     * 增
     */
    public void add(int index, E element){

        if (size == data.length){
            throw new IllegalArgumentException("数组已满,无法添加元素");
        }

        if (index < 0 || index > size){
            throw new IllegalArgumentException("索引值非法");
        }

        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }

        data[index] = element;
        size++;
    }

    public void addFirst(E element){

        add(0, element);

    }

    public void addLast(E element){

        add(size, element);
    }

    /**
     * 删
     */
    public void remove(int index){

        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("索引值非法");
        }

        for (int i = index; i < size - 1; i++) {
            data[i] = data[i + 1];
        }

        size--;

        /**
         * 泛型存储的是引用类型,删除只是断开了指针,但不会被回收,会占用存储空间,称为loitering objects
         * 因此最好手动将其赋值为null,让JVM进行回收
         */
        data[size] = null;
    }

    public void removeFirst(){

        remove(0);
    }

    public void removeLast(){

        remove(size - 1);
    }

    /**
     * 改
     */
    public void set(int index, E element){

        if (index < 0 || index > size){
            throw new IllegalArgumentException("索引值非法");
        }

        data[index] = element;
    }

    /**
     * 查
     */
    public int find(E element){

        for (int i = 0; i < size; i++) {

            if (data[i].equals(element)){
                return i;
            }
        }
        return -1;
    }

    public E get(int index){

        if (index < 0 || index > size) {
            throw new IllegalArgumentException("索引值非法");
        }

        return data[index];
    }

    public boolean contains(E element){

        for (int i = 0; i < size; i++) {

            if (data[i].equals(element)) {
                return true;
            }
        }
        return false;
    }

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

    /**
     * 获取数组的容量
     * 容量是固定的,而元素个数是可变的
     */
    public int getCapacity(){
        return data.length;
    }

    /**
     * 判断数组是否为空
     */
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 重写toString()方法,打印数组的元素
     */
    @Override
    public String toString() {

        StringBuilder str = new StringBuilder();

        str.append(String.format("数组的容量为%d,元素个数为%d\n", data.length, size));
        str.append("[");

        for (int i = 0; i < size; i++) {

            str.append(data[i]);

            if (i != size - 1){
                str.append(", ");
            }
        }

        str.append("]");

        return str.toString();
    }
}

/**
 * 对自定义类不仅要比较是否相同,还需要进行排序,则需要实现Comparable接口,重写compareTo()方法
 */
class Student implements Comparable{

    private String name;
    private int score;

    public Student(String name, int score){

        this.name = name;
        this.score = score;
    }

    @Override
    public int compareTo(Object obj) {

        Student newObj = (Student) obj;

        return this.score - newObj.score;
    }

    @Override
    public String toString() {

        return String.format("%s,成绩是%d", name, score);
    }
}

动态数组

Java自带的数组属于静态数组,最大的缺点是不能实现扩缩容,并且必须先指定容量

而动态数组就是从底层实现扩缩容,但本节实现的动态数组本质还是使用了静态的数据结构

public class Algorithm {

    public static void main(String[] args) {

        Array<String> arr = new Array<>(5);

        for (int i = 0; i < 10; i++) {
            arr.addLast(String.format("第%d个元素", i));
        }

        /**
         * 添加元素个数超过了容量,会进行扩容
         */
        System.out.println(String.format("数组的容量为%d", arr.getCapacity()));
        System.out.println(arr);

        for (int i = 0; i < 5; i++) {
            arr.remove(0);
        }

        /**
         * 删除元素导致个数少于容量的一半,会进行缩容
         */
        System.out.println(String.format("数组的容量为%d", arr.getCapacity()));
        System.out.println(arr);
    }
}

class Array<E> {

    private E[] data;
    private int size;

    public Array(int capacity){

        data = (E[]) new Object[capacity];
        size = 0;
    }

    public Array(){
        
        this(10);
    }

    public void add(int index, E element){

        if (index < 0 || index > size){
            throw new IllegalArgumentException("索引值非法");
        }

        /**
         * 当数组满了以后,容量翻倍
         */
        if (size == data.length){
            resize(2 * data.length);
        }

        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }

        data[index] = element;
        size++;
    }

    public void addLast(E element){
        
        add(size, element);
    }

    public void remove(int index){

        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("索引值非法");
        }

        for (int i = index; i < size - 1; i++) {
            data[i] = data[i + 1];
        }

        size--;
        data[size] = null;

        /**
         * 当数组元素个数少于容量的一半时,容量缩小一半,节省空间
         */
        if (size == data.length / 2 && data.length / 2 != 0){
            resize(data.length / 2);
        }
    }

    public int getCapacity(){
        
        return data.length;
    }

    /**
     * 定义扩缩容方法
     */
    private void resize(int newCapacity){

        E[] newData = (E[]) new Object[newCapacity];

        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        
        data = newData;
    }

    @Override
    public String toString() {

        StringBuilder str = new StringBuilder();

        str.append(String.format("数组的容量为%d,元素个数为%d\n", data.length, size));
        str.append("[");

        for (int i = 0; i < size; i++) {

            str.append(data[i]);
            if (i != size - 1){
                str.append(", ");
            }
        }

        str.append("]");

        return str.toString();
    }
}

动态数组的时间复杂度

增删操作,需要遍历整个数组,时间复杂度为O(n)

改查操作,如果是根据索引来,复杂度为O(1),否则也是O(n)

均摊复杂度

addLast()方法中调用了resize()方法,因此时间复杂度为O(n)

但事实上只有当元素个数超出容量时才会扩容,平均下来相当于每次操作的时间复杂度为O(2)

因此addLast()方法的均摊复杂度为O(1),这比总是考虑最坏的情况更有实际意义

image

防止复杂度震荡

上例中,均摊复杂度虽然为O(1),但有一种最坏的情况,那就是增加一个元素进行扩容以后,马上又删除该元素进行缩容,如此反复循环的话,复杂度仍会变成O(n),这称为复杂度震荡

为了防止这种情况,可以改为元素个数少于容量的1/4时再减半缩容,这样不会短时间反复出现扩缩容

image

image

public void remove(int index){

    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("索引值非法");
    }

    for (int i = index; i < size - 1; i++) {
        data[i] = data[i + 1];
    }

    size--;
    data[size] = null;

    if (size == data.length / 4  && data.length / 2 != 0){
        resize(data.length / 2);
    }
}
posted @ 2021-10-12 16:49  振袖秋枫问红叶  阅读(108)  评论(0)    收藏  举报