看得見的資料結構Android版之陣列表(資料結構篇)

張風捷特烈發表於2018-11-22
零、前言:

一講到裝東西的容器,你可能習慣於使用ArrayList和陣列,你有想過ArrayList和陣列的區別嗎?
Java的類起名字都不是隨便亂起的,一般前面是輔助,後面是實質:ArrayList = Array + List
Array就是陣列,List便是表結構,ArrayList即陣列實現的表結構,問題來了,什麼是表結構
注:不要問我效果圖用什麼軟體畫的...因為本來就沒用什麼軟體畫。下一節帶你一起自己畫!!!
希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star

0.不管別的,先留圖鎮樓:

表結構的常規操作

表結構的常規操作.gif

陣列的擴容與縮容

陣列的擴容與縮容


1.在我們生活中都有什麼表?
課程表,成績表,作息時間表、列車行程表、手錶(這個算了吧...)
複製程式碼
2.表有什麼用?

成績表.jpg

可以把同類的物件統一管理,比如成績表:
高三12班的54為同學的成績是物件,物件又包括數學、語文、英語...等屬性  
把混亂的54個物件放在一起,這麼一排,哪個是學霸,哪個是學渣一目瞭然,非常方便  
如果其中某個物件的成績改錯了,可以get到物件重新set一下,也非常方便  
如果中間一個人作弊了,分數取消,直接remove掉,後面的人名詞往前排,也非常方便  
如果高三12班和高三11班要比較成績,兩張表contact一下,就成一張表,合併也很方便
複製程式碼
3.表和陣列有什麼不同?

打個最恰當的比方就是:陣列相當於列印出來的紙質版表結構像是Excel中可操作版

1.陣列定長:新增新元素,定位新增都很困難
2.拿刪除來說:陣列remove掉了,後面的人名次都不變----(我還沒個空白名次高,你說氣人不氣人...)
3.表是一種抽象資料型別(Abstract Data Type),既然是抽象就是規範或功能,表會有不同的實現形式 

[番外]:小和尚問老和尚:"什麼是聖人?"    老和尚說:"好好學習,天天向上,樂於助人,誠信友善"
這裡"聖人"便是一種抽象,"好好學習,天天向上,樂於助人,誠信友善"便是"聖人""條件(功能)",
小和尚按照這麼做了,他就是老和尚眼中的"聖人",即小和尚實現了聖人。

4.同樣,表是一種抽象,也可以定義你眼中的表,併為它附上add(),get(),set(),remove()等功能  
5.其實Java的ArrayList實現了List這個抽象介面
複製程式碼
4.陣列表結構:本文要務

陣列表結構.png


一、定義自己的表結構

由於Java用List,為了不混淆,取了個新名字叫Chart

1.定義表的介面

也就是說說你的表能幹嘛用(介面方法最好註釋非常清晰)

/**
 * 作者:張風捷特烈
 * 時間:2018/9/25 0025:8:25
 * 郵箱:1981462002@qq.com
 * 說明:線性表結構介面
 */
public interface IChart<T> {
//region -------------新增操作------------

    /**
     * 定點新增
     *
     * @param index 索引
     * @param el    資料元素
     */
    void add(int index, T el);

    /**
     * 新增尾
     *
     * @param el 資料元素
     */
    void add(T el);

//endregion

//region -------------刪除操作------------

    /**
     * 定位刪除
     *
     * @param index 索引
     * @return 刪除的元素
     */
    T remove(int index);

    /**
     * 刪除尾位
     *
     * @return 刪除的元素
     */
    T remove();

    /**
     * 刪除指定元素的第一次出現時
     *
     * @param el 資料元素
     * @return 元素位置
     */
    int removeEl(T el);

    /**
     * 刪除所有指定元素
     *
     * @param el 資料元素
     */
    boolean removeEls(T el);

    /**
     * 清空集合
     */
    void clear();

//endregion

//region -------------改查操作------------

    /**
     * 設定某位置的元素新值
     *
     * @param index 索引
     * @param el    新值
     * @return 舊值
     */
    T set(int index, T el);

    /**
     * 根據指定位置獲取元素
     *
     * @param index 索引
     * @return 資料元素
     */
    T get(int index);

    /**
     * 根據指定元素獲取匹配索引
     *
     * @param el 資料元素
     * @return 索引集
     */
    int[] getIndex(T el);
//endregion

//region ------------其他操作-------------

    /**
     * 集合是否包含某元素
     *
     * @param el 資料元素
     * @return 是否包含
     */
    public boolean contains(T el);

    /**
     * 連線兩個集合
     *
     * @param iChart 插入集合
     * @return 合併後的集合
     */
    public IChart<T> contact(IChart<T> iChart);

    /**
     * 定點連線兩個集合
     *
     * @param index  索引
     * @param iChart 插入集合
     * @return 合併後的集合
     */
    IChart<T> contact(int index, IChart<T> iChart);

    /**
     * 是否為空
     *
     * @return 是否為空
     */
    boolean isEmpty();

    /**
     * 返回集合大小
     *
     * @return 大小
     */
    int size();
    
    /**
     * 獲取陣列容量
     * @return 陣列容量
     */
    int capacity();

//endregion
}
複製程式碼
2.使用陣列實現表結構:ArrayChart

實現介面,並實現介面裡的所有方法

/**
 * 作者:張風捷特烈<br/>
 * 時間:2018/11/21 0021:8:18<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:陣列實現線性表結構
 */
public class ArrayChart<T> implements IChart<T> {
 //空實現---略
}
複製程式碼
3.成員變數和構造初始化
private int size;//表中資料的個數
private T[] data;//資料核心承載體
private static final int DEFAULT_CAPACITY = 10;//預設陣列容量
private static final float GROW_RATE = 1.5f;//擴容增長率

public ArrayChart() {
    this(DEFAULT_CAPACITY);//無參構造預設建立10個容量的陣列
}
public ArrayChart(int capacity) {
    data = (T[]) new Object[capacity];//新建立[陣列表]時初始化陣列
}
複製程式碼
4.簡單介面的實現:
@Override
public boolean isEmpty() {
    return size == 0;
}

@Override
public int size() {
    return size;
}

@Override
public int capacity() {
    return data.length;
}
複製程式碼

二、主要方法的實現(CRUD)

1.定點新增元素:

看一下操作圖(將在下一篇:檢視篇完成):預設新增到尾部
思路:定點後的所有元素後移一位,空出頂點位,讓待新增元素入駐
紫色框代表空的陣列位,中間填充的是表中的實際元素
可見定點新增是在選中索引的前一位新增,所以新增到尾部是add(size,data)來新增

尾新增和定點新增.gif

@Override
public void add(T el) {
    add(size , el);//這裡size---是因為在size之前一位新增
}

@Override
public void add(int index, T el) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed! Make sure index < 0 || index > size");
    }
    //從最後一個元素開始,到定點位置元素,元素都後移一位
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = el;
    size++;
}
複製程式碼

addByIndex.png


2.查詢與設定值:get(),set():

set和定索引查詢.gif

 @Override
 public T set(int index, T el) {
     if (index < 0 || index >= size) {
         throw new IllegalArgumentException("Set failed! Make sure index < 0 || index > size");
     }
     T oldEl = get(index);
     data[index] = el;//設定一下,很簡單
     return oldEl;
 }

 @Override
 public T get(int index) {
     if (index < 0 || index >= size) {
         throw new IllegalArgumentException("Get failed! Make sure index < 0 || index > size");
     }
     return data[index];//查詢陣列的對應索引處
 }
複製程式碼

定值查詢獲取索引

定值查詢獲取索引.gif

@Override
public int[] getIndex(T el) {
    int[] tempArray = new int[size];//臨時陣列
    int count = 0;//重複個數
    for (int i = 0; i < size; i++) {//遍歷集合,獲取該元素重複個數,及位置陣列
        if (data[i].equals(el)) {
            tempArray[count] = i;
            count++;
        }
    }
    //將臨時陣列壓縮---排除空位
    int[] indexArray = new int[count];
    for (int i = 0; i < count; i++) {
        indexArray[i] = tempArray[i];
    }
    return indexArray;//返回查詢元素的索引陣列(相當於成績表看數學80分的都有哪些人)
}
複製程式碼

3.刪除元素:

刪除和定點刪除.gif

@Override
public T remove() {
    return remove(size - 1);
}

@Override
public T remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Remove failed! Make sure index < 0 || index > size");
    }
    T temp = get(index);
    //從刪除元素索引的下一位開始到結尾,依次左移
    // 可簡寫: System.arraycopy(data, index + 1, data, index + 1 - 1, size - index + 1);
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    size--;
    //置空--遊蕩的物件
    data[size] = null;
    return temp;
}
複製程式碼

其他刪除: 定元素刪除單個和定元素刪除所有相當於前面的組合操作,就不做操作演示了

@Override
public int removeEl(T el) {
    int[] indexes = getIndex(el);//查詢元素的索引集合,刪除首個
    int index = -1;
    if (indexes.length > 0) {
        index = indexes[0];
        remove(indexes[0]);
    }
    return index;
}
@Override
public boolean removeEls(T el) { //查詢元素的索引集合,刪除所有
    int[] indexArray = getIndex(el);
    if (indexArray.length != 0) {
        for (int i = 0; i < indexArray.length; i++) {
            remove(indexArray[i] - i); // 注意-i
        }
        return true;
    }
    return false;
}
複製程式碼

三、動態擴容與縮容的實現

也沒有什麼高大上的,就是一個籃子裝不下了,裝個更大的籃子裝而已

陣列的擴容與縮容

1.擴容與縮容方法的實現
/**
 * 擴容/縮容
 *
 * @param newCapacity 新容量
 */
private void grow(int newCapacity) {
    T[] newData = (T[]) new Object[newCapacity];//建立個大籃子
    for (int i = 0; i < size; i++) {//把原來的元素放到大籃子裡
        newData[i] = data[i];
    }
    data = newData;
}
複製程式碼
2.擴容和縮容的呼叫契機

什麼時候擴容----籃子不夠裝了唄---add
什麼時候需要縮容----1000個容量的籃子裝1個雞蛋想想也浪費---remove

1) add檢測擴容時機:滿了
@Override
public void add(int index, T el) {
    if (size == data.length) {//籃子裝不下了---
        grow((int) (GROW_RATE * data.length));//換個1.5倍的籃子
    }
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed! Make sure index < 0 || index > size");
    }
    //從最後一個元素開始,到定點位置元素,元素都後移一位
    //可簡寫:System.arraycopy(data, index, data, index + 1, size - index);
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = el;
    size++;
}
複製程式碼
2)remove檢測縮容時機

這裡的判斷標誌是留多點備用,不然有可能插入移除頻繁而導致重複擴容或縮容,
籃子可能會說:"你到底縮還是放,你是不是在玩老子....,老子給你多留點空行了吧!"

@Override
public T remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Remove failed! Make sure index < 0 || index > size");
    }
    T temp = get(index);
    //從刪除元素索引的下一位開始到結尾,依次左移
    // 可簡寫: System.arraycopy(data, index + 1, data, index + 1 - 1, size - index + 1);
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    size--;
    //置空--遊蕩的物件
    data[size] = null;
    //縮容----此處限定是為了避免反覆出現擴容縮容---可自定義
    if (size == data.length / 4 && data.length / 2 != 0 && data.length > 5) {
        grow(data.length / 2);
    }
    return temp;
}
複製程式碼
3.清空時,陣列縮放到初始值
@Override
public void clear() {
    size = 0;
    grow(DEFAULT_CAPACITY);
}
複製程式碼

四、其他操作

1.是否包含某元素
 @Override
 public boolean contains(T el) {
     return getIndex(el).length != 0;//按值查詢有資料
 }
複製程式碼

2.contact連線陣列

表的聯合.png

@Override
public IChart<T> contact(IChart<T> iChart) {
    return contact(size - 1, iChart);
}


@Override
public IChart<T> contact(int index, IChart<T> iChart) {
    if (!(iChart instanceof ArrayChart)) {//必須是陣列才能聯合
        return null;
    }
    //從index處遍歷本陣列,將待插入資料一個一個插入
    for (int i = index; i < index + iChart.size(); i++) {
        add(i + 1, iChart.get(i - index));
    }
    return this;
}
複製程式碼

作為一個表結構,基本上就演示這麼多,還有其他操作可以自定義介面,自己實現,
不過不管多麼複雜的操作都是以上操作的組合而已。


五、小結:

關於複雜度的分析,等到所有表結構講完再整體比較一下,這裡先粗略感覺一下

耗時測試
方法\操作次數 1000 10000 10W 100W 1000W
add首 0.0063秒 0.2706秒 19.5379秒 ---- ----
add尾 0.0004秒 0.0025秒 0.0141秒 0.0687秒 1.26014秒
remove首 0.0063秒 0.2771秒 19.7902秒 ---- ----
remove尾 0.0005秒 0.0036秒 0.0091秒 0.02301秒 :0.1607秒

可以看出往開始新增/刪除會很困難,從程式碼中可以感覺到,畢竟要讓後面所有人挪一挪
想一下如果30000人排一起,第一個人走了,後面所有人往前挪一下,是不是工程量挺大的
要是你決定插到第一個,讓後面的人都往後移一下.....(大哥,活著難道不好嗎....)
所以頻繁對第一個元素進行操作的,還是不要作死,陣列表結構(ArrayList)不適合你


本系列後續更新連結合集:(動態更新)


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-21 看得見的資料結構Android版之表的陣列實現(資料結構篇)
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章