程式設計師內功修煉之資料結構

零星發表於2019-11-17

哈哈

陣列

   陣列是指用一組地址連續的儲存線性表的資料元素。陣列能夠順序(按照存入的順序)儲存相同型別的多個資料。訪問陣列中的某個元素的方法是將其編號然後通過編號訪問,這個編號我們稱其為索引。如果有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

相關文章