数据结构系列(1)— 数组

507 阅读15分钟

学 无 止 境 , 与 君 共 勉 。


相关系列

概要

数组是应用最广泛的一种数据结构,大部分的编程语言中都存在这种数据结构。通常数组内的数据是可以允许重复的,如果需要实现数组数据不重复,则需要在新增的时候去和已有的数据进行比对,判断是否已存在目标元素,当数组很大的时候,每次新增都要进行大量比对,效率低下。这里不考虑数据不重复的情况

这里通过自制的数组类来展示数组新增、删除、查询的简单实现方式;

无序数组

数组内部没有排序,每次新增都是在末尾加入元素;删除、查询需要遍历数组去比对数据。

定义数组

public class Array {
    /**
     * 存放数据
     */

    private long[] arr;

    /**
     * 当前元素的个数
     * 最大索引为 elemNu -1
     */

    private int elemNu;

    /**
     * 初始化数组
     *
     * @param max 最大长度
     */

    public Array(int max) {
        this.arr = new long[max];
        this.elemNu = 0;
    }
}

新增

直接在当前数组的末尾添加,只需一步操作;

    /**
     * 在当前空闲位新增,并将数组个数+1
     *
     * @param elem 新增的元素
     */

    public void insert(long elem) {
        this.arr[this.elemNu++] = elem;
    }

删除

这个例子的数组允许存在重复数据,所以删除可以分为只删除首次查询的到元素和删除数组中的所有目标元素两种情况情况

删除首个匹配到的目标元素

先遍历数组,找到要删除的元素的索引,然后把该元素之后的元素统一向前移动一位

    public boolean deleteOne(long elem) {
        // 要删除元素的索引
        int delIndex;
        for (delIndex = 0; delIndex < this.elemNu; delIndex++) {
            if (elem == this.arr[delIndex]) {
                break;
            }
        }
        // 不存在,直接返回false
        if (delIndex == this.elemNu) {
            return false;
        }
        // 元素个数-1
        this.elemNu--;
        // 将后面的元素向前移动一位
        for (int newIndex = delIndex; newIndex < this.elemNu; newIndex++) {
            this.arr[newIndex] = this.arr[newIndex + 1];
        }
        return true;
    }

删除所有匹配的元素

JAVA 的 ArrayList中没有直接的方法,可以通过循环和迭代器的方式实现

    // 使用普通for循环遍历 , 删除元素 a
    for (int i = 0; i < list.size(); i++) {
        // 匹配元素.
        if ("a".equals(list.get(i))) {
            list.remove(i);
            // 重点!!!
            i--;
        }
    }
    // 使用倒叙循环
    for (int i = list.size() - 1; i >= 0; i--) {
        if ("a".equals(list.get(i))) {
            list.remove(i);
        }
    }
    // 迭代器删除
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String next = iterator.next();
        if ("a".equals(next)) {
            iterator.remove();
        }
    }
    // 迭代器简化版
    list.removeIf("a"::equals);

这里通过重新定义一个新数组将旧数组中未删除的元素按顺序添加进来,然后将原来的数据指向这个新数组的方式去实现。这样可以避免每删除一个元素就把后面的元素重新位移一次,提高效率。

     public boolean deleteAll(long elem) {
        // 重新定义
        long[] newArr = new long[this.arr.length];
        // 删除的数量
        int delNu = 0;
        // 新的索引
        int newIndex = 0;
        for (int index = 0; index < this.elemNu; index++) {
            if (elem == this.arr[index]) {
                delNu++;
            } else {
                newArr[newIndex++] = this.arr[index];
            }
        }
        // 目标元素没有匹配到,删除失败
        if (delNu == 0) {
            return false;
        }
        this.elemNu -= delNu;
        this.arr = newArr;
        return true;
    }

查询

遍历元素,比对是否存在目标元素,如果匹配到的目标元素索引大于数组最大索引,则表示不存在

    public boolean contains(long elem) {
        int index;
        for (index = 0; index < this.elemNu; index++) {
            if (elem == this.arr[index]) {
                break;
            }
        }
        return index < this.elemNu;
    }

有序数组

有序数组的内部数据按照升序,降序的方式排列,因此可以通过二分法快速的查询到目标元素,查询、删除效率要高于无需数组;新增的时候需要计算目标元素的索引位置,因此插入效率低于无需元素。它的核心功能在于查询,新增、删除都需要用到。

定义数组

public class OrderArray {
    /**
     * 存放数据
     */

    private long[] arr;

    /**
     * 当前元素的个数
     * 最大索引为 elemNu -1
     */

    private int elemNu;

    /**
     * 初始化数组
     *
     * @param max 最大长度
     */

    public OrderArray(int max) {
        this.arr = new long[max];
        this.elemNu = 0;
    }
}

查询

通过二分法快速查询目标元素的索引,删除操作时,如果没有匹配的目标元素,返回-1,新增时,如果已存在相同的元素,返回对应元素的索引,如果不存在,则返回最后一次查询定位的索引

    /**
     * 核心功能:二分法查询,新增、删除都调用
     * @param elem     目标元素
     * @param isInsert 是否用于新增
     * @return 索引
     */

    public int find(long elem, boolean isInsert) {
        // 查询的下界
        int lowerBound = 0;
        // 查询的上界
        int upperBound = this.elemNu - 1;
        // 当前查询的索引
        int curIndex;
        while (true) {
            // 每次取中间的数据
            curIndex = (lowerBound + upperBound) / 2;
            //  相等,直接返回索引
            if (this.arr[curIndex] == elem) {
                return curIndex;
            }
            // 查询下界 > 查询上界,说明没有查询到
            if (lowerBound > upperBound) {
                // 新增,返回最终定位的索引
                if (isInsert) {
                    return curIndex;
                }
                return -1;
            }
            // 中间值小于目标值,说明目标在后半部分数据中(即大的数据)
            if (this.arr[curIndex] < elem) {
                lowerBound = curIndex + 1;
            } else {
                // 中间值大于目标值,说明目标在前半部分数据中
                upperBound = curIndex - 1;
            }
        }
    }

新增

如果新增插入的元素是在首尾的,直接可以在对应的索引出插入。如果不是首尾位置的,通过二分法查询出最终定位的索引,如果该处的元素值比目标元素小,则目标元素的插入的位置为最终定位元素+1。插入后目标元素之后的元素需要往后移动一位,防止原来数据被覆盖掉,这里需要从尾部开始移动

    public void insert(long elem) {
        // 要插入的索引
        int insertIndex;
        if (this.elemNu == 0 || elem < this.arr[0]) {
            // 在头部插入
            insertIndex = 0;
        } else if (elem > this.arr[this.elemNu - 1]) {
            // 在尾部插入
            insertIndex = this.elemNu;
        } else {
            // 在中间插入
            insertIndex = find(elem, true);
            // 定位点的元素比目标元素小,插入点往后移动一位
            if (this.arr[insertIndex] < elem) {
                insertIndex++;
            }
        }
        // 元素数量+1
        this.elemNu++;
        // 插入位置后面的元素往后以一个位置,防止原来的数据被覆盖从尾部开始移动
        for (int newIndex = this.elemNu - 1; newIndex > insertIndex; newIndex--) {
            this.arr[newIndex] = this.arr[newIndex - 1];
        }
        // 插入目标元素
        this.arr[insertIndex] = elem;
    }

删除

同样存在删除首个匹配到的目标元素和删除所有匹配的元素两种情况

删除首个匹配到的目标元素

直接二分法查询目标元素,存在,则将目标元素之后元素的索引向前移动一位

    public boolean deleteOne(long item) {
        int index = find(item, false);
        if (index == this.elemNu) {
            return false;
        }
        this.elemNu--;
        for (int i = index; i < this.elemNu; i++) {
            this.arr[i] = this.arr[i + 1];
        }
        return true;
    }

删除所有匹配的元素

有顺数组的相同元素都在一起,通过二分法查询获取到元素索引和,判断该索引的前后位置是否是相同的元素,这样可以获取到目标元素存在的索引区间和个数,将后面的元素向前移动对应的位置。相比无需数组效率要高。

    public boolean deleteAll(long elem) {
        // 查询到的目标元素索引
        int findIndex = find(elem, false);
        if (findIndex < 0) {
            return false;
        }
        // 目标元素的首位索引
        int startIndex = findIndex;
        // 目标元素的末位索引
        int endIndex = findIndex;
        // 要删除的数据个数
        int delNu = 1;
        // 定位开始索引
        while (startIndex > 0 && this.arr[startIndex - 1] == elem) {
            startIndex--;
            delNu++;
        }
        // 定位结束索引
        while (endIndex < this.elemNu - 1 && this.arr[endIndex + 1] == elem) {
            endIndex++;
            delNu++;
        }
        // 总元素减少
        this.elemNu -= delNu;
        // 后面的数据前移N位
        for (int newIndex = startIndex; newIndex < this.elemNu; newIndex++) {
            this.arr[newIndex] = this.arr[++endIndex];
        }
        return true;
    }

结尾

有序数组的查找速度比无序数组快多了,适合于频繁查找的场景下使用;
有序数组插入操作由于靠后的元素需要后移,所以速度较慢;
有序和无序数组删除操作都很慢,因为需要靠后的元素向前移动;

访问源码

本系列所有代码均上传至Github上,方便大家访问

>>>>>> 数据结构-数组 <<<<<<

日常求赞

创作不易,如果各位觉得有帮助,求点赞支持

求关注