• 数据结构与算法——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字节的整数倍,所以不足的要补齐

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

    随机访问

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

    2.动态数组

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

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

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

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

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

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

    3.动态数组的实现

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

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

    下面给出具体的代码:

    1. import java.util.Arrays;
    2. import java.util.Iterator;
    3. import java.util.function.Consumer;
    4. import java.util.stream.IntStream;
    5. /**
    6. * 动态数组
    7. * */
    8. public class L1_DynamicArray implements Iterable{
    9. //定义的基础数组,这是一种懒定义模式,即一开始不给出数组的长度
    10. private int[] array = {};
    11. //定义变量,记录数组的有效值
    12. private int size = 0;
    13. //在数组某位添加元素的方法,变量为要添加的值
    14. public void addLast(int element){
    15. // 就直接让size出的值等于要添加的值
    16. // array[size] = element;
    17. // 然后size++就可以了
    18. // size++;
    19. //这里整合了代码,直接调用下面的方法,在末尾添加元素本质上就是在size处添加元素
    20. add(size,element);
    21. }
    22. //在目标索引index出添加元素element
    23. public void add(int index,int element){
    24. //扩容逻辑:
    25. checkAndGrow();
    26. //下面是添加逻辑:
    27. //首先判断index是否符合要求,符合要求,OK,index后面的元素挪位置
    28. if(index >= 0 && index <= size){
    29. /**这个方法的参数的含义如下:
    30. * 第一次参数:你要复制那个数组中的元素
    31. * 第二个参数:你要复制那个数组中的元素的起始位置
    32. * 第三个参数:你要复制到哪个数组中,即复制的数据最终放哪
    33. * 第四个参数:移动到的目标的起始位置是哪里
    34. * 第五个参数:要移动多少个参数?
    35. * */
    36. System.arraycopy(array,index,array,index+1,size-index);
    37. //赋值操作
    38. array[index] = element;
    39. //有效值+1
    40. size++;
    41. }else {
    42. System.out.println("索引出错");
    43. }
    44. }
    45. private void checkAndGrow(){
    46. if(size == 0){
    47. array = new int[2];
    48. }else if(size == array.length){
    49. int L = array.length * 2;
    50. int[] newArray = new int[L];
    51. System.arraycopy(array,0,newArray,0,size);
    52. array = newArray;
    53. }
    54. }
    55. //获取index处的元素
    56. public int get (int index){
    57. return array[index];
    58. }
    59. //最简单最基础最死板的遍历
    60. public void forI(){
    61. for (int i = 0; i < size; i++) {
    62. System.out.println(array[i]);
    63. }
    64. }
    65. /**
    66. * 上面的遍历是写死的,只实现了打印的功能,但是有时我们可能需要这个元素去做别的事情,所以这里的遍历最好不要写死
    67. * 所以我们就使用函数是接口来写
    68. * 使用函数式接口的时候我们需要考虑两个问题:我们能给这个接口传递什么?我们需要从这个接口得到什么?
    69. * 根据上面的两个问题,我们使用consumer这个接口
    70. */
    71. public void foreach(Consumer consumer){
    72. for (int i = 0; i < size; i++) {
    73. consumer.accept(array[i]);
    74. }
    75. }
    76. //迭代器遍历
    77. //使用迭代器遍历最常用的方法就是实现一个接口,然后实现接口里面的方法
    78. @Override
    79. public Iterator iterator() {
    80. return new Iterator() {
    81. /**下面两个方法的使用场景一般是:
    82. * 在一个循环中不断的调用hasNext方法,如果有那就不断循环,如果没有下一个元素,那就退出循环
    83. * 然后在循环内部再不断的调用next方法
    84. */
    85. //定义游标 i
    86. int i = 0;
    87. @Override
    88. //遍历着去询问有没有下一个元素,有就返回true,没有就返回false
    89. public boolean hasNext() {
    90. //当i小于size时,表示有下一个元素,那就返回真
    91. return i < size;
    92. }
    93. @Override
    94. //返回当前的元素,并将指针移动到下一个元素
    95. public Integer next() {
    96. //返回当前的元素i,然后游标后移
    97. return array[i++];
    98. }
    99. };
    100. }
    101. //使用流的方式对其进行遍历
    102. public IntStream stream(){
    103. return IntStream.of(Arrays.copyOfRange(array,0,size));
    104. }
    105. //删除的方法
    106. public int remove(int index){
    107. //用变量来接收要删除的元素
    108. int removed = array[index];
    109. //元素移位操作,这里对index的位置没有进行判断,是省略了,加上一个if判断一下也是可以的
    110. System.arraycopy(array,index+1,array,index,size-index-1);
    111. //有效值--
    112. size--;
    113. //返回要删除的元素
    114. return removed;
    115. }
    116. /**
    117. * 性能:
    118. * 头部插入或删除:O(n)
    119. * 中间插入或删除:O(n)
    120. * 尾部插入或删除:O(1)(均摊来说)
    121. * */
    122. }

     下面给出测试代码

    测试结果就不展示了

    4.二维数组

    先给出二维数组的语法:

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

    算了,还是说一下吧

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

    5.合并两个数组

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

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

    6.总结

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

  • 相关阅读:
    自己动手写一个Golang ORM框架
    《HelloGitHub》第 90 期
    [sd-tagging-helper] How to start and how to add tags for dummies
    【数理方程】分离变量法
    替换Jar包中文件,重打Jar包
    第2-4-8章 规则引擎Drools实战(1)-个人所得税计算器
    Redis群集
    使用Netty进行协议开发:多协议支持与自定义协议的实现
    IronSource 聚合广告平台接入踩坑日记——游戏声音消失
    Apache Hop Pipeline Transforms【持续完善中】
  • 原文地址:https://blog.csdn.net/m0_52096593/article/details/132105312