1. 程式人生 > >【從今天開始好好學資料結構01】陣列

【從今天開始好好學資料結構01】陣列

面試的時候,常常會問陣列和連結串列的區別,很多人都回答說,“連結串列適合插入、刪除,時間複雜度O(1);陣列適合查詢,查詢時間複雜度為O(1)”。實際上,這種表述是不準確的。陣列是適合查詢操作,但是查詢的時間複雜度並不為O(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是O(logn)。所以,正確的表述應該是,陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為O(1)。

每一種程式語言中,基本都會有陣列這種資料型別。不過,它不僅僅是一種程式語言中的資料型別,還是一種最基礎的資料結構。儘管陣列看起來非常基礎、簡單,但是我估計很多人都並沒有理解這個基礎資料結構的精髓。在大部分程式語言中,陣列都是從0開始編號的,但你是否下意識地想過,為什麼陣列要從0開始編號,而不是從1開始呢? 從1開始不是更符合人類的思維習慣嗎?帶著這個問題來學習接下來的內容,帶著問題去學習往往效果會更好!!!

什麼是陣列?我估計你心中已經有了答案。不過,我還是想用專業的話來給你做下解釋。陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。這個定義裡有幾個關鍵詞,理解了這幾個關鍵詞,我想你就能徹底掌握陣列的概念了。下面就從我的角度分別給你“點撥”一下。

第一是線性表(Linear List)。顧名思義,線性表就是資料排成像一條線一樣的結構。每個線性表上的資料最多隻有前和後兩個方向。其實除了陣列,連結串列、佇列、棧等也是線性表結構。而與它相對立的概念是非線性表,比如二叉樹、堆、圖等。之所以叫非線性,是因為,在非線性表中,資料之間並不是簡單的前後關係。

第二個是連續的記憶體空間和相同型別的資料。正是因為這兩個限制,它才有了一個堪稱“殺手鐗”的特性:“隨機訪問”。但有利就有弊,這兩個限制也讓陣列的很多操作變得非常低效,比如要想在陣列中刪除、插入一個數據,陣列為了保持記憶體資料的連續性,會導致插入、刪除這兩個操作比較低效,相反的陣列查詢則高效

陣列java程式碼:

package array;

/**
 * 1) 陣列的插入、刪除、按照下標隨機訪問操作;
 * 2)陣列中的資料是int型別的;
 *
 * Author: Zheng
 * modify: xing, Gsealy
 */
public class Array {
    //定義整型資料data儲存資料
    public int data[];
    //定義陣列長度
    private int n;
    //定義中實際個數
    private int count;

    //構造方法,定義陣列大小
    public Array(int capacity){
        this.data = new int[capacity];
        this.n = capacity;
        this.count=0;//一開始一個數都沒有存所以為0
    }

    //根據索引,找到資料中的元素並返回
    public int find(int index){
        if (index<0 || index>=count) return -1;
        return data[index];
    }

    //插入元素:頭部插入,尾部插入
    public boolean insert(int index, int value){
        //陣列中無元素 

        //if (index == count && count == 0) {
        //    data[index] = value;
        //    ++count;
        //    return true;
        //}

        // 陣列空間已滿
        if (count == n) {
            System.out.println("沒有可插入的位置");
            return false;
        }
        // 如果count還沒滿,那麼就可以插入資料到陣列中
        // 位置不合法
        if (index < 0||index > count ) {
            System.out.println("位置不合法");
            return false;
        }
        // 位置合法
        for( int i = count; i > index; --i){
            data[i] = data[i - 1];
        }
        data[index] = value;
        ++count;
        return true;
    }
    //根據索引,刪除陣列中元素
    public boolean delete(int index){
        if (index<0 || index >=count) return false;
        //從刪除位置開始,將後面的元素向前移動一位
        for (int i=index+1; i<count; ++i){
            data[i-1] = data[i];
        }
        //刪除陣列末尾元素  這段程式碼不需要也可以
        /*int[] arr = new int[count-1];
        for (int i=0; i<count-1;i++){
            arr[i] = data[i];
        }
        this.data = arr;*/

        --count;
        return true;
    }
    public void printAll() {
        for (int i = 0; i < count; ++i) {
            System.out.print(data[i] + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Array array = new Array(5);
        array.printAll();
        array.insert(0, 3);
        array.insert(0, 4);
        array.insert(1, 5);
        array.insert(3, 9);
        array.insert(3, 10);
        //array.insert(3, 11);
        array.printAll();
    }
}

GenericArray陣列程式碼

public class GenericArray<T> {
    private T[] data;
    private int size;

    // 根據傳入容量,構造Array
    public GenericArray(int capacity) {
        data = (T[]) new Object[capacity];
        size = 0;
    }

    // 無參構造方法,預設陣列容量為10
    public GenericArray() {
        this(10);
    }

    // 獲取陣列容量
    public int getCapacity() {
        return data.length;
    }

    // 獲取當前元素個數
    public int count() {
        return size;
    }

    // 判斷陣列是否為空
    public boolean isEmpty() {
        return size == 0;
    }

    // 修改 index 位置的元素
    public void set(int index, T e) {
        checkIndex(index);
        data[index] = e;
    }

    // 獲取對應 index 位置的元素
    public T get(int index) {
        checkIndex(index);
        return data[index];
    }

    // 檢視陣列是否包含元素e
    public boolean contains(T e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return true;
            }
        }
        return false;
    }

    // 獲取對應元素的下標, 未找到,返回 -1
    public int find(T e) {
        for ( int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return i;
            }
        }
        return -1;
    }


    // 在 index 位置,插入元素e, 時間複雜度 O(m+n)
    public void add(int index, T e) {
        checkIndex(index);
        // 如果當前元素個數等於陣列容量,則將陣列擴容為原來的2倍
        if (size == data.length) {
            resize(2 * data.length);
        }

        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;
    }

    // 向陣列頭插入元素
    public void addFirst(T e) {
        add(0, e);
    }

    // 向陣列尾插入元素
    public void addLast(T e) {
        add(size, e);
    }

    // 刪除 index 位置的元素,並返回
    public T remove(int index) {
        checkIndexForRemove(index);

        T ret = data[index];
        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) {
            resize(data.length / 2);
        }

        return ret;
    }

    // 刪除第一個元素
    public T removeFirst() {
        return remove(0);
    }

    // 刪除末尾元素
    public T removeLast() {
        return remove(size - 1);
    }

    // 從陣列中刪除指定元素
    public void removeElement(T e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(String.format("Array size = %d, capacity = %d \n", size, data.length));
        builder.append('[');
        for (int i = 0; i < size; i++) {
            builder.append(data[i]);
            if (i != size - 1) {
                builder.append(", ");
            }
        }
        builder.append(']');
        return builder.toString();
    }


    // 擴容方法,時間複雜度 O(n)
    private void resize(int capacity) {
        T[] newData = (T[]) new Object[capacity];

        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    private void checkIndex(int index) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed! Require index >=0 and index <= size.");
        }
    }

    private void checkIndexForRemove(int index) {
        if(index < 0 || index >= size) {
            throw new IllegalArgumentException("remove failed! Require index >=0 and index < size.");
        }
    }
}

到這裡,就回溯最初的問題:

從陣列儲存的記憶體模型上來看,“下標”最確切的定義應該是“偏移(offset)”。前面也講到,如果用a來表示陣列的首地址,a[0]就是偏移為0的位置,也就是首地址,a[k]就表示偏移k個type_size的位置,所以計算a[k]的記憶體地址只需要用這個公式:

a[k]_address = base_address + k * type_size

但是,如果陣列從1開始計數,那我們計算陣列元素a[k]的記憶體地址就會變為:

a[k]_address = base_address + (k-1)*type_size

對比兩個公式,我們不難發現,從1開始編號,每次隨機訪問陣列元素都多了一次減法運算,對於CPU來說,就是多了一次減法指令。那你可以思考一下,類比一下,二維陣列的記憶體定址公式是怎樣的呢?有興趣的可以在評論區評論出來哦QAQ

陣列作為非常基礎的資料結構,通過下標隨機訪問陣列元素又是其非常基礎的程式設計操作,效率的優化就要儘可能做到極致。所以為了減少一次減法操作,陣列選擇了從0開始編號,而不是從1開始。
不過我認為,上面解釋得再多其實都算不上壓倒性的證明,說陣列起始編號非0開始不可。所以我覺得最主要的原因可能是歷史原因。

關於陣列,它可以說是最基礎、最簡單的資料結構了。陣列用一塊連續的記憶體空間,來儲存相同型別的一組資料,最大的特點就是支援隨機訪問,但插入、刪除操作也因此變得比較低效,平均情況時間複雜度為O(n)。在平時的業務開發中,我們可以直接使用程式語言提供的容器類,但是,如果是特別底層的開發,直接使用陣列可能會更合適。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回覆!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...

相關推薦

今天開始好學資料結構01陣列

面試的時候,常常會問陣列和連結串列的區別,很多人都回答說,“連結串列適合插入、刪除,時間複雜度O(1);陣列適合查詢,查詢時間複雜度為O(1)”。實際上,這種表述是不準確的。陣列是適合查詢操作,但是查詢的時間複雜度並不為O(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是O(logn)。所以,正確的表述

今天開始好學資料結構02棧與佇列

目錄 1、理解棧與佇列 2、用程式碼談談棧 3、用程式碼談談佇列 我們今天主要來談談“棧”以及佇列這兩種資料結構。 回顧一下上一章中【資料結構01】陣列中,在陣列中只要知道資料的下標,便可通過順

今天開始好學資料結構03連結串列

目錄 今天我們來聊聊“連結串列(Linked list)”這個資料結構。 在我們上一章中【從今天開始好好學資料結構02】棧與佇列棧與佇列底層都是採用順序儲存的這種方式的,而今天要聊的連結串列則是採用鏈式儲存,連結串列可以說是繼陣列之後第二種使用得最廣泛的通用資

今天開始好學資料結構04程式設計師你心中就沒點“樹”嗎?

目錄 樹(Tree) 二叉樹(Binary Tree) 前面我們講的都是線性表結構,棧、佇列等等。今天我們講一種非線性表結構,樹。樹這種資料結構比線性表的資料結構要複雜得多,內容也比較多,首先我們先從樹(Tre

開始/親測國內外均可基於阿里雲Ubuntu的kubernetes(k8s)主從節點分散式叢集搭建——分步詳細攻略v1.11.3準備工作篇

從零開始搭建k8s叢集——香港節點無牆篇【大陸節點有牆的安裝方法我會在每一步操作的時候提醒大家的注意,並告訴大家如何操作】 由於容器技術的火爆,現在使用K8s開展服務變得越來越廣泛了。 本攻略是基於阿里雲主機搭建的一個單主節點和單從節點的最簡k8s分散式叢集。 為了製作

資料結構01緒論-考研程式碼書寫規範與語言基礎

決定將一些內容敲成部落格以供自己複習和使用 僅供參考。 ///////////////////////////////////////////////////////////////////////

開始學架構-李運華07|低成本、安全、規模

低成本 高效能和高可用架構通常都是增加伺服器來滿足要求,但低成本正相反,當然也不是首要目標。 往往“創新”才能達到低成本的目標!! 技術創新: NoSQL(Memcache、Redis)等是為了解決關係型資料庫無法應對高併發帶來的訪問壓力。

開始學架構-李運華03|架構設計的目的

架構設計的誤區     系統不一定需要架構設計;     架構設計不一定能提升開發效率;     好的架構設計能促進業務發展;     不是所有系統都需要架構設計;     等等…… 架構設計的真正目的     為了解決軟體複雜度帶來的問題 如何下手架構設計?

開始學架構-李運華10|架構設計流程:識別複雜度

架構設計第一步:識別複雜度 架構設計的本質目的是為了解決系統複雜性,所以要先了解。 【例】一個系統的複雜度來源於業務邏輯複雜,功能耦合度嚴重,架構師設計TPS達到50000/s的高效能架構沒有意義。 出現問題主要為了滿足“高可用”“高效能”“可擴充套件”三

開始學架構-李運華06|複雜地來源:可擴充套件性

可擴充套件性指系統為了應對將來需求的變化而提供的一種擴充套件能力,新需求出現時系統不需要或者僅需要少量修改就可以支援,無需整個系統重構或者重建。 面向物件就是為了解決可擴充套件性,後來的設計模式更是將可擴充套件性做到了極致。 具備良好擴充套件性的

今天開始學習資料結構(c++/c)---連結串列

先實現這麼多功能,後續再填程式碼(本人一菜渣,c式鬧著玩程式設計如下): #include <iostream> using namespace std; struct Node{ int value; Node *next;

[今天開始修煉資料結構]線性表及其實現以及實現有Itertor的ArrayList和LinkedList

一、線性表   1,什麼是線性表   線性表就是零個或多個數據元素的有限序列。線性表中的每個元素只能有零個或一個前驅元素,零個或一個後繼元素。在較複雜的線性表中,一個數據元素可以由若干個資料項組成。比如牽手排隊的小朋友,可以有學號、姓名、性別、出生日期等資料項。   2,線性表的抽象資料型別   線性表的抽象

[今天開始修煉資料結構]棧、斐波那契數列、逆波蘭四則運算的實現

一、棧的定義   棧是限定僅在表尾進行插入和刪除操作的線性表。允許插入和刪除的一端稱為棧頂(top),另一端稱為棧底(bottom)。棧又稱後進先出的線性表,簡稱LIFO結構。   注意:首先它是一個線性表,也就是說棧元素有前驅後繼關係。   棧的插入操作,叫做進棧,也稱壓棧、入棧   棧的刪除操作,叫做出棧

[今天開始修煉資料結構]佇列、迴圈佇列、PriorityQueue的原理及實現

[從今天開始修煉資料結構]基本概念 [從今天開始修煉資料結構]線性表及其實現以及實現有Itertor的ArrayList和LinkedList [從今天開始修煉資料結構]棧、斐波那契數列、逆波蘭四則運算的實現 [從今天開始修煉資料結構]佇列、迴圈佇列、PriorityQueue的原理及實現 一、什麼是佇列  

[今天開始修煉資料結構]串、KMP模式匹配演算法

[從今天開始修煉資料結構]基本概念 [從今天開始修煉資料結構]線性表及其實現以及實現有Itertor的ArrayList和LinkedList [從今天開始修煉資料結構]棧、斐波那契數列、逆波蘭四則運算的實現 [從今天開始修煉資料結構]佇列、迴圈佇列、PriorityQueue的原理及實現 一、什麼是串?  

[今天開始修煉資料結構]樹,二叉樹,線索二叉樹,霍夫曼樹

前面我們已經提到了線性表,棧,佇列等資料結構,他們有一個共同的特性,就是結構中每一個元素都是一對一的,可是在現實中,還有很多一對多的情況需要處理,所以我們需要研究這種一對多的資料結構 —— 樹,並運用它的特性來解決我們在程式設計中遇到的問題。 一、樹的定義   1,樹Tree是n(n >= 0) 個結點

[今天開始修煉資料結構]圖

我們之前介紹了線性關係的線性表,層次關係的樹形結構,下面我們來介紹結點之間關係任意的結構,圖。一、相關概念   1,圖是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。   2,各種圖定義    若兩頂點之間的邊沒有方向,則稱這

[今天開始修煉資料結構]圖的最小生成樹 —— 最清楚易懂的Prim演算法和kruskal演算法講解和實現

接上文,研究了一下演算法之後,發現大話資料結構的程式碼風格更適合與前文中鄰接矩陣的定義相關聯,所以硬著頭皮把大話中的最小生成樹用自己的話整理了一下,希望大家能夠看懂。   一、最小生成樹     1,問題       最小生成樹要解決的是帶權圖 即 網 結構的問題,就是n個頂點,用n-1條邊把一個連通圖連線起

[今天開始修煉資料結構]圖的最短路徑 —— 迪傑斯特拉演算法和弗洛伊德演算法的詳解與Java實現

在網圖和非網圖中,最短路徑的含義不同。非網圖中邊上沒有權值,所謂的最短路徑,其實就是兩頂點之間經過的邊數最少的路徑;而對於網圖來說,最短路徑,是指兩頂點之間經過的邊上權值之和最少的路徑,我們稱路徑上第一個頂點是源點,最後一個頂點是終點。 我們講解兩種求最短路徑的演算法。第一種,從某個源點到其餘各頂點的最短路徑

[今天開始修煉資料結構]無環圖的應用 —— 拓撲排序和關鍵路徑演算法

上一篇文章我們學習了最短路徑的兩個演算法。它們是有環圖的應用。下面我們來談談無環圖的應用。   一、拓撲排序     博主大學學的是土木工程,在老本行,施工時很關鍵的節約人力時間成本的一項就是流水施工,鋼筋沒綁完,澆築水泥的那幫兄弟就得在那等著,所以安排好流水施工,讓工作週期能很好地銜接就很關鍵。這樣的工程活