数据结构与算法——9.数组

这篇文章,我们来介绍一下第一个数据结构——数组

目录

1.概述

1.1定义

1.2 性能

2.动态数组

3.动态数组的实现

4.二维数组

5.合并两个数组

6.总结



1.概述

在java基础部分,我们已经介绍过数组,那时候介绍的数组侧重于介绍数组的创建与使用,在涉及到底层的方面上,讲述的比较少,而这里再次介绍数组将侧重于在底层方面上介绍数组的一些特点。

1.1定义

数组的定义:在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识

注意:

数组中的元素在内存中是连续存储的,索引数组中元素的地址,可以通过其索引计算出来。比如我们知道了数组的数据的起始位置为BaseAddress,可以由公式BaseAddress+i*size计算出索引 i 元素的地址(i 即索引,在java、c等语言都是从0开始的;size是每个元素占用的字节,例如 int 占用4,double占8)

1.2 性能

下面来分析一下一个数组的空间占用情况

java中数组的结构为:

  • 8字节的markword
  • 4字节class指针(压缩class指针的情况)
  • 4字节数组大小(决定了数组最大容量为2^32)
  • 数组元素+对齐字节(java中所有对象大小都是8字节的整数倍,不足的要补齐)

举例说明:int[ ] array = {1,2,3,4,5}

空间占用大小为40字节,组成如下:8+4+4+5*4+4=40

解释:

  • 数组是一个对象,对象名要占用空间,即8字节
  • 要有类型指针,即指明这是什么类型的数组用的,这个占4字节
  • 然后还有记录数组大小的值,占4个字节(这也就意味着数组的最大容量为2^32)
  • 最后就是补齐的空间了,因为java中所有对象大小都是8字节的整数倍,所以不足的要补齐

我们看一下下面的这张图:

数据结构与算法——9.数组_第1张图片

随机访问

因为数组的根据索引来查找元素的,所以数组访问的时间复杂度为O(1) 

2.动态数组

我们前面所学的和所使用的数组都是静态的数组,一经创建,它的容量是固定的,无法改变,下面我们来学习一下动态数组

首先,我们来思考一下动态数组是什么样的

首先,会给你一个数组,它里面是空的,你可以往里面放东西,也可以删除内容,也可以插入内容。等到它装满了,我们还要装,那这个数组就要扩容,假设新容量是原乡容量的两倍,然后将原数组中的内容拷贝到新数组中,然后就可以继续添加内容了

下面,思考一下应该怎么实现

首先,肯定有一个数组作为基本数组,然后有一个变量来记录数组里面有效值的个数。最开始的时候,我们没有添加值,所以有效值为0,然后我们往数组里面添加值,在添加的时候我们要判断有效值的大小是否小于数组的大小,如果小,则可以添加,如果大,则扩容拷贝再添加,扩容拷贝java中有方法。删除的时候要判断删除的位置是否是尾位置,如果是尾位置,则直接令有效值减1,如果要删除的元素在中间,则先要将后面的元素都往前面挪一位,然后再令有效值减1,更改和查看都是直接根据索引来的,遍历直接一个for循环就OK了,插入和添加的逻辑相似,只不过多了一步拷贝挪位的过程。

OK,思路有了,下面来实现一下动态数组

3.动态数组的实现

下面来看一下动态数组的实现

具体的讲解可以看上面的思路和代码中的注释

数据结构与算法——9.数组_第2张图片

下面给出具体的代码:

import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.stream.IntStream;

/**
 * 动态数组
* */
public class L1_DynamicArray implements Iterable{
    //定义的基础数组,这是一种懒定义模式,即一开始不给出数组的长度
    private int[] array = {};
    //定义变量,记录数组的有效值
    private int size = 0;


    //在数组某位添加元素的方法,变量为要添加的值
    public void addLast(int element){
//        就直接让size出的值等于要添加的值
//        array[size] = element;
//        然后size++就可以了
//        size++;
        //这里整合了代码,直接调用下面的方法,在末尾添加元素本质上就是在size处添加元素
        add(size,element);
    }


    //在目标索引index出添加元素element
    public void add(int index,int element){
        //扩容逻辑:
        checkAndGrow();

        //下面是添加逻辑:
        //首先判断index是否符合要求,符合要求,OK,index后面的元素挪位置
        if(index >= 0 && index <= size){
            /**这个方法的参数的含义如下:
             * 第一次参数:你要复制那个数组中的元素
             * 第二个参数:你要复制那个数组中的元素的起始位置
             * 第三个参数:你要复制到哪个数组中,即复制的数据最终放哪
             * 第四个参数:移动到的目标的起始位置是哪里
             * 第五个参数:要移动多少个参数?
             * */
            System.arraycopy(array,index,array,index+1,size-index);
            //赋值操作
            array[index] = element;
            //有效值+1
            size++;
        }else {
            System.out.println("索引出错");
        }
    }

    private void checkAndGrow(){
        if(size == 0){
            array = new int[2];
        }else if(size == array.length){
            int L = array.length * 2;
            int[] newArray = new int[L];
            System.arraycopy(array,0,newArray,0,size);
            array = newArray;
        }
    }

    //获取index处的元素
    public int get (int index){
        return array[index];
    }

    //最简单最基础最死板的遍历
    public void forI(){
        for (int i = 0; i < size; i++) {
            System.out.println(array[i]);
        }
    }

    /**
     * 上面的遍历是写死的,只实现了打印的功能,但是有时我们可能需要这个元素去做别的事情,所以这里的遍历最好不要写死
     * 所以我们就使用函数是接口来写
     * 使用函数式接口的时候我们需要考虑两个问题:我们能给这个接口传递什么?我们需要从这个接口得到什么?
     * 根据上面的两个问题,我们使用consumer这个接口
     */
    public void foreach(Consumer consumer){
        for (int i = 0; i < size; i++) {
            consumer.accept(array[i]);
        }
    }

    //迭代器遍历
    //使用迭代器遍历最常用的方法就是实现一个接口,然后实现接口里面的方法
    @Override
    public Iterator iterator() {
        return new Iterator() {
            /**下面两个方法的使用场景一般是:
             * 在一个循环中不断的调用hasNext方法,如果有那就不断循环,如果没有下一个元素,那就退出循环
             * 然后在循环内部再不断的调用next方法
            */
            //定义游标 i
            int i = 0;

            @Override
            //遍历着去询问有没有下一个元素,有就返回true,没有就返回false
            public boolean hasNext() {
                //当i小于size时,表示有下一个元素,那就返回真
                return i < size;
            }
            @Override
            //返回当前的元素,并将指针移动到下一个元素
            public Integer next() {
                //返回当前的元素i,然后游标后移
                return array[i++];
            }
        };
    }

    //使用流的方式对其进行遍历
    public IntStream stream(){
        return IntStream.of(Arrays.copyOfRange(array,0,size));
    }

    //删除的方法
    public int remove(int index){
        //用变量来接收要删除的元素
        int removed = array[index];
        //元素移位操作,这里对index的位置没有进行判断,是省略了,加上一个if判断一下也是可以的
        System.arraycopy(array,index+1,array,index,size-index-1);
        //有效值--
        size--;
        //返回要删除的元素
        return removed;
    }
    /**
     * 性能:
     * 头部插入或删除:O(n)
     * 中间插入或删除:O(n)
     * 尾部插入或删除:O(1)(均摊来说)
     * */
}

 下面给出测试代码

数据结构与算法——9.数组_第3张图片

测试结果就不展示了

4.二维数组

先给出二维数组的语法:

数据结构与算法——9.数组_第4张图片

然后能够弄清索引就行了。至于二维数组的内存情况,这个会分析对象的内存情况就会分析这个的内存情况。

算了,还是说一下吧

二维数组的本质还是多个一维数组。我们知道数组是一个对象,是对象就有其地址值。所以我们二维数组中实际存的就是每个一维数组的地址值,注意这些地址值可能是不连续的,但是这些地址值的地址值是连续的,然后那些可能不连续的地址值又标识着一些一维数组,这样就构成了一个二维数组。这就是二维数组在内存中的本质。

5.合并两个数组

下面看一下如何合并两个数组:

数据结构与算法——9.数组_第5张图片

这个其实并不难,看代码应该可以看懂 

6.总结

这篇文章主要讲了数组,重点侧重于动态数组的一些操作,其实都是很简单的内容,不复杂。

你可能感兴趣的:(数据结构与算法,算法,开发语言,java)