程序员内功修炼之数据结构

250 阅读8分钟

哈哈

数组

   数组是指用一组地址连续的存储线性表的数据元素。数组能够顺序(按照存入的顺序)存储相同类型的多个数据。访问数组中的某个元素的方法是将其编号然后通过编号访问,这个编号我们称其为索引。如果有N个值,它们的编号是0至N-1(索引的取值范围)。

数组

1. 数组的创建与动态数组

1.1 创建数组

  在Java中,数组被当做是对象,在创建的时候通过关键字new来完成,书写方式有如下两种:

int[] array = new int[10];
int array[] = new int[10];

  对于编译器来说通过[]来判断他正在命名的是数组对象,而不是普通变量。

  在上面的两种创建方式中,制定了数组中存储的元素类型,与数组的大小。此时数组一经创建,大小固定。当s使用到超过数组范围的位置时,就会有java.lang.ArrayIndexOutOfBoundsException

1.2 数组中数据的访问

  数组是一个对象,它的名字是数组的一个引用,并不是数组本身,数组存储在内存中的其他地址中,通过前面代码中的array来保存这个引用。

  我们通过数组索引的方式可以获取到存储在内存地址中某一位置的元素array[Index]。或者通过array[index] = XXX的方式赋值或修改该位置元素的值。

1.3 动态数组

  在申明数组的时,需要指定数组的名称和它包含的类型,在创建数组时,需要指定数组的长度。指定数组的长度后,若改长度不可改变,这就一意味着,改数组中不能存放更多的元素。因此,我们需要动态的更新数组容量的大小。

 通过对Java中数组进一步封装,来模拟创建动态数组,定义自己的Array类,并提供一些基础的构造方法与判断方法。

public class Array<E> {
    /**
     * 存储数组中元素
     */
    private E[] data;

    /**
     * 数组中存储元素的个数
     */
    private int size;

    /**
     * 有参构造函数
     * @param capacity 数组容量
     */
    public Array(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    /**
     * 传入数组方式构建动态数组
     * @param arr
     */
    public Array(E[] arr){
        data = (E[]) new Object[arr.length];
        for (int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        size = arr.length;
    }

    /**
     * 无参构造函数 默认数组容量为10
     */
    public Array() {
        this(10);
    }

    /**
     * 获取数组的大小
     * @return 数组大小
     */
    public int getSize(){
        return size;
    }

    /**
     * 获取数组的容量
     * @return 静态数组的data的length
     */
    public int getCapacity(){
        return data.length;
    }

    /**
     * 判断数组是否为空
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }
}
1.3.1 初始化
//通过无惨构造器,创建默认长度的数组
Array<Integer> intArray = new Array<>();
//创建初始容量为20的数组
Array<Integer> intArray = new Array<>(20);
//通过传入数组的方式创建
String arrStr[] = {"1","2"};
Array<String> arr = new Array<>(arrStr);

2. 动态数组的操作

  动态数组的动态是指其容量的动态变化,当容量快满时,进行扩容操作,使得数组在添加元素时不至于索引越界。当数组中的元素个数远小于数组的容量时,减小数组的容量,不至于内存的浪费。

容量变化

  约定:当数组中元素的个数达到数组的长度时,数组容量变为原来容量的1.5倍;当数组中元素的个数为数组容量的1/4 时,数组的容量变为原来的1/2;这里扩容时都会用到下面的resize(int newCapacity)方法。这个约定在后面数组中添加元素的方法中会有所体现,敬请期待。。。

 private void resize(int newCapacity){
        E[] newData = (E[])new Object[newCapacity];
        for(int i = 0; i < size; i++){
            newData[i] =data[i];
        }
        data = newData;
 }
2.1 添加元素

  在Java中对数组元素的添加时通过操作索引的方式对数组完成元素的加入,在这里对动态数组的操作也是通过索引的方式来添加元素,不过在添加的时候要对数组中元素的个数与数组的容量进行动态的调整。

 /**
  * 在某一索引插入某一元素
  * @param e     插入的元素
  * @param index 插入元素的位置
  */
public void addIndex(E e,int index){
    if(index < 0 || index > size){
        throw new ArrayIndexOutOfBoundsException("index out of bound")
    }
    if(size == data.length){
        resize(data.length + (data.length >> 1));
    }
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = e;
    size ++;
}

  在对上述的方法进行封装,提供在第一个索引位置添加元素addFirst(E e)和在所有元素的最后面添加一个元素addLast(E e)的方法。

添加元素

  /**
  *  向所有元素后面添加一个元素
  * @param e 要插入的元素
  */
public void addLast(E e){
  addIndex(e,size);
}

 /**
  * 在第一个位置插入一个元素
  * @param e
  */
public void addFirst(E e){
  addIndex(e,0);
}

  可以看出,在数组中某一索引位置添加元素的方式,是将索引中该位置及后面的元素统统的向后移动,这里采用的是从最后一个索引位置向要添加位置遍历后移的方式来完成的。最后在将要添加的值,添加到该位置。其中resize(data.length + (data.length >> 1))这句话表明,新数组的容量为元素组容量的1.5倍。

2.2 删除元素

  同理删除元素也是通过索引将该位置及该位置后面的所有元素,都用通过前移该原数组中对应位置的后一个元素来实现的。

/**
* 删除索引位置的元素
* @param index
* @return 返回删除的元素
*/
public E remove(int index){
  if(index < 0 || index >= size){
    throw new ArrayIndexOutOfBoundsException("index out of bound");
  }
  E res = data[index];
  for(int i = index; i < size - 1; i++){
    data[i] =data[i+1];
  }
  data[size - 1] = null;
  size --;
  if(size == data.length / 4){
    resize(data.length / 2);
  }
  return res;
}

  其中size == data.length / 4resize(data.length / 2)就是在删除元素的时候判断数组中元素的个数与数组容量的关系来完成缩小数组容量的操作。

删除元素

2.3 修改元素

  修改某一位置的元素,就是将新的元素替换原来旧的元素即可。

/**
* 修改某一索引的元素
* @param e    元素
* @param index 索引
*/
public void update(E e,int index){
  if(index < 0 || index >= size){
    throw new ArrayIndexOutOfBoundsException();
  }
  data[index] = e;
}
2.4 查找元素

  首先,这里的查找元素按照更一般的数组定义(这里数组中的元素之间没有特定的大小关系)来查找。后面会介绍一种特殊的查找方式(二分查找),这种基于数组的二分查找,是有一定的要求的(数组中的元素存在大小关系)。

  在数组中判断某个元素是不是在数组中,通过遍历数组的方式获取数组中每一个位置的元素,与要查找的元素比较是否相同来判断。

/**
* 数组中是否包含元素e
* @param e
* @return
*/
public boolean contains(E e){
  for(int i = 0; i < size; i++){
    if(data[i].equals(e)){
      return true;
    }
  }
  return false;
}

3. 再论查找

3.1 线性查找

  如上所述的查找,在查找一个元素时,要遍历数组中所用的元素,依次判断数组的中元素与要查找的元素是否相同来完成。这个查找称之为线性查找。大多数组的查找都是通过这种方式来完成的。

3.2 基于数组的二分查找
二分查找基础

  首先介绍一个概念:有序数组。其中的数据是按关键字升序(或降序)排列的。这种排列方式可以使用二分查找快速的查找数据项。这里有一个小的知识点要注意:使用数组存储数据的方式,要进行二分查找。数组中的元素是要符合大小排列的顺序(由大到小或小到大)。

二分查找原理

  介绍原理的时,假设:数组中的元素是有序的,即在数组中索引较小的位置,存储的元素的值较小。数组中的元素之间是按照从小到大的顺序排列的。

如:要在[1,2,3,4,5,6,7,8]这个数组中查找元素4

  1. 找到(数组)中间索引位置的元素;

  2. 判断中间位置元素与待查找元素的大小;

    a. 待查找元素大于中间元素,在数组后半部分查找

    b. 待查找元素小于中间元素,在数组前半部分中查找;

  3. 重复上述步骤1、2;直到找到或者数组无法在分割。

    二分查找

代码实现
/**
* 
* @param searchKey 要查找的元素
* @return 存在:返回数组中元素所在下标
*         不存在:返回-1  
*/
public int binarySearch(Integer searchKey){
        int low = 0;
        int high = array.getSize() - 1;
        while (low <= high){
            int mid = (low + high) / 2;
            if(e.compareTo(array.getIndex(mid)) < 0 ){
                high = mid - 1;
            }else if(e.compareTo(array.getIndex(mid)) > 0){
                low = mid + 1;
            }else {
                return mid;
            }
        }
        return -1;
    }

  二分查找是每次查找将数组的长度变为原来的1/2,直到数组无法分割返回。

3.3 对比与总结
  1. 首先分析二分查找与线性查找。线性查找要进行遍历判所有数据查找;二分查找,通过每次都将待查找部分的数组元素数据减半的方式来减少查找次数,这种查找方式在打的数据量下效果尤为明显,但前提不能忘,那就是数组中的元素有序。

  2. 可以看到线性查找与二分查找的本质区别是:数组是否有序。有序数组的有点与缺点也是同样的明显。

    2.1 有序数组的查找速度在大数据量时要优于无序数组。

    2.2 序数组要插入要将插入元素后面的元素向后移动来维护数组的有序性。

  3. 数组的删除,无论数组是否有序,都需要将删除位置之后的元素前移,来维护内存地址的连续性。


个人微信公众号:

个人github:

github.com/FunCheney