fish-stack / Algo

算法相关的话题
0 stars 0 forks source link

数组 #20

Open bitfishxyz opened 5 years ago

bitfishxyz commented 5 years ago

数组

我是学习Python和JS入门编程的,在学习数据结构的阶段,就在掉到坑里了。原因之一就是我很难理解数组这种数据结构。

在Python中的List或者JS中的Array,它们其实是一个非常复杂包装类,隐藏了真正的数组的特征,让我对数组这种结构产生了很多误解。

导致我在小白的时候一直有疑问:

好吧,现在看来这些想法有点可笑,为了避免有些同学有和我一样的误区,我们整理了数组这个结构的几个特点。

一个数组占用内存是连续的

这点很好理解,作为一种线性的数据结构,它需要占用连续的内存

数组中每个元素应该占用同样长度的内存

其实在C、Java这种偏底层的静态编程语言中,都是要求数组中的每个元素是同一个类型的。为什么有这样要求?

这是为了数组的随机访问。

假设一个数组中每个元素都是占用8个字节的内存,其中第一个元素在内存中的起始地址是0000 aaaa,那么请问这个数组中第7个元素的内存起始地址是多少?

很简单,应该是 0000 aaaa 向后数56个字节, 是这个地址0000 aae2,就是这么简单。

然是如果如果数组中允许每个元素占用不同大小的内存,会反生什么情况呢? 但是你是我们就无法做到随机访问了。

这个有一个推论,一致第一个元素的第一,和某一元素的索引,就可以推断出 它的地址。

CPU访问内存中每个地址消耗的时间相同

这就是由计算机的构造原理决定了,访问@0000 0000@ffff ffff 消耗的时间一样。 具体的原理我也不是很清楚,反正就是什么总线啊什么乱七八糟的概念。但是结论很很重要。

这个就是说,我们访问数组中第一个元素和最后一个元素消耗的时间是一样。。。。

结合上面一点,我们就是说数组支持随机访问。

数组容量固定

一但创建了一个数组,它的容量就确定了。 [1, 2, 3], 那么它在内存中就是占用12个字节。不能变化。

如果我想删除2这个元素,怎么把? 不能使不能直接删除,然后变成[1, 3]这样的,但是我们有三种办法

只有这三种方法。

小节

上面就是对数组这种数据结构的理解。然后就是学习数据结构的时候 建议使用C Java,不建议使用JS,Python

bitfishxyz commented 5 years ago

Java实现

Java中就是原生数组,我们就以Java语言为例,来测试下上面的步骤

这是我们数组

        String[] arr =  new String[3];
        arr[0] = "a";
        arr[1] = "b";
        arr[2] = "c";

现在我们想把索引为1的元素的值设置为"d",我们该怎么做呢?

很简单,利用数组的随机访问原理,我们可以直接修改

        // 修改元素
        arr[1] = "d";

如果我们想删除索引为2的元素,我们该怎么办呢? 显然,数组的创建,它的大小不能再修改,我们只能重新创建一个数组

        // 删除索引为2的元素
        String[] arr2 = new String[arr.length - 1];
        arr2[0] = arr[0];
        arr2[1] = arr[1];

这样就行了。我们要创建一个新的数组,然后把原来数组的其他元素复制到新的数组中, 那么我们就是删除了原来的数组了。 现在我们可以把arr2看做是arr删除了一个元素后的新状态。

同理,如果我们想在索引为0的位置添加一个新的元素,我们可以

        // 在索引为0的位置添加一个元素e
        String[] arr3 = new String[arr2.length + 1];
        arr3[0] = "e";
        arr3[1] = arr2[0];
        arr3[2] = arr2[1];

长度为3的,如果想对它进行增删给查旧的按照我们执勤啊说的步骤, 所以我们可以写一个包装类,来包装上面的操作。

不过我们可以卡看到,Java的原生数组太底层了,用的时候不方便, 所以我们可以写一个包装类,来方便我们增删改查

通过Array类来包装

import lombok.Getter;
import lombok.ToString;

@ToString
public class Array<E> {
    private static int increaseCapacityTimes = 2;

    /**
     * 实际的容量
     */
    @Getter
    private int size;

    /**
     * 底层数组
     */
    private E[] data;

    public Array(int capacity){
        data = (E[])new Object[capacity];
    }
    public Array(){
        this(10);
    }

    /**
     * 底层数组的最大容量
     */
    public int getCapacity(){
        return data.length;
    }
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 添加底层数组容量
     */
    private void increaseCapacity(){
        E[] newData = (E[])new Object[data.length * increaseCapacityTimes];
        for (int i = 0; i < data.length; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    /**
     * 降低底层数组容量
     */
    private void decreaseCapacity(){
        E[] newData = (E[])new Object[data.length / 2 + 1];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    /**
     * 修改数组的容量
     */
    private void resize(){
        if (size == data.length){
            increaseCapacity();
        }
        if (size <= data.length / 3 && data.length > 20){
            decreaseCapacity();
        }
    }

    /**
     * 假设数组为 [11, 22, 33, 44]
     * insert(99, 2)后,数组变为
     * [11, 22, 99, 33, 44]
     * 原始数组索引为[2, length)都要向后移动
     * 新插入原始的索引为2
     * @param e
     * @param index: 可以取到size
     */
    public void insert(E e, int index){
        resize();
        if (index > size || index < 0) {
            throw new IllegalArgumentException("索引应该在[0, size]之间");
        }
        for (int i = size; i > index; i--) {
            data[i] = data[i-1];
        }
        data[index] = e;
        size++;
    }
    public void addLast(E e){
        insert(e, size);
    }
    public void addFirst(E e){
        insert(e, 0);
    }
    public boolean contains(E e){
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)){
                return true;
            }
        }
        return false;
    }

    /**
     * 寻找当前元素的位置,如果不存在就返回-1
     * @param e
     * @return
     */
    public int indexOf(E e){
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)){
                return i;
            }
        }
        return -1;
    }

    /**
     * 删除指定索引位置的元素,并将这个元素的值返回
     * @param index
     * @return
     *
     * data[size]这个位置的值还在,但是没有关系的
     */
    public E remove(int index){
        assertIndex(index);
        resize();
        E currentValue = data[index];
        for (int i = index; i < size - 1 ; i++) {
            data[i] = data[i + 1];
        }
        size--;
        // 释放资源
        data[size] = null;
        return currentValue;
    }
    public E removeLast(){
        return remove(size - 1);
    }
    public E removeFirst(){
        return remove(0);
    }

    public boolean removeElement(E e){
        int index = indexOf(e);
        if (index != -1) {
            remove(index);
            return true;
        }
        return false;
    }

    /**
     * 获得底层数组索引为index的位置的元素
     * @param index
     * @return
     */
    public E get(int index){
        assertIndex(index);
        return data[index];
    }
    public E getLast(){
        return get(size - 1);
    }
    public E getFirst(){
        return get(0);
    }

    public void set(E e, int index){
        assertIndex(index);
        data[index] = e;
    }

    private void assertIndex(int index){
        if (index >= size || index < 0) {
            throw new IllegalArgumentException("索引应该在[0, size)之间");
        }
    }
}