為什麼先講陣列
資料結構可以簡單的被分為線性結構和非線性結構。
線性結構大致包括:
- 陣列(連續儲存);
- 連結串列(離散儲存);
- 棧(線性結構常見應用,由連結串列或陣列增刪和改進功能實現);
- 佇列(線性結構常見應用,由連結串列或陣列增刪和改進功能實現);
非線性結構大致包括:
- 樹;
- 圖;
其中,陣列是應用最廣泛的資料儲存結構。它被植入到大部分程式語言中。由於陣列十分容易懂,所以它被用來作為介紹資料結構的起點非常合適。
JavaScript陣列基礎知識
在ECMAScript中陣列是非常常用的引用型別了。ECMAScript所定義的陣列和其他語言中的陣列有著很大的區別。那麼首先要說的就是陣列在js中是一種特殊的物件。
特點:
- 陣列是一組資料的線性集合;
- js陣列更加類似java中的容器。長度可變,元素型別也可以不同;
- 陣列的長度可以隨時修改(length屬性);
常用操作方法:
- push、pop
- shift、unshift
- splice、slice
- concat、join、sort、reverse等
JavaScript陣列操作
一、 陣列方法:
1、 陣列的建立
1 2 3 4 |
var array = []; var array = new Array(); //建立一個陣列 var array = new Array([size]); //建立一個陣列並指定長度,注意不是上限,是長度 var array = new Array([element0[, element1[, ...[, elementN]]]]); //建立一個陣列並賦值 |
注意:雖然第三種方法建立陣列指定了長度,但實際上所有情況下陣列都是變長的,也就是說即使指定了長度為5,仍然可以將元素儲存在規定長度以外的,並且這時長度會隨之改變。
2、 陣列元素的訪問
1 2 |
var getArrItem=array[1]; //獲取陣列的元素值 array[1]= "new value"; //給陣列元素賦予新的值 |
3、 陣列元素的新增
1 2 3 4 5 |
array. push([item1 [item2 [. . . [itemN ]]]]);// 將一個或多個新元素新增到陣列結尾,並返回陣列新長度 array.unshift([item1 [item2 [. . . [itemN ]]]]);// 將一個或多個新元素新增到陣列開始,陣列中的元素自動後移,返回陣列新長度 array.splice(insertPos,0,[item1[, item2[, . . . [,itemN]]]]);//將一個或多個新元素插入到陣列的指定位置,插入位置的元素自動後移,返回""。 |
4、 陣列元素的刪除
1 2 3 4 5 6 7 |
array.pop(); //移除最後一個元素並返回該元素值 array.shift(); //移除最前一個元素並返回該元素值,陣列中元素自動前移 array.splice(deletePos,deleteCount); //刪除從指定位置deletePos開始的指定數量deleteCount的元素,陣列形式返回所移除的元素 array.slice(start, [end]); //以陣列的形式返回陣列的一部分,注意不包括 end 對應的元素,如果省略 end 將複製 start 之後的所有元素 |
5、 陣列的合併
1 |
array.concat([item1[, item2[, . . . [,itemN]]]]); //將多個陣列(也可以是字串,或者是陣列和字串的混合)連線為一個陣列,返回連線好的新的陣列 |
6、 陣列的拷貝
1 2 3 |
array.slice(0); //返回陣列的拷貝陣列,注意是一個新的陣列,不是指向 array.concat(); //返回陣列的拷貝陣列,注意是一個新的陣列,不是指向 |
7、 陣列元素的排序
1 2 3 |
array.reverse(); //反轉元素(最前的排到最後、最後的排到最前),返回陣列地址 array.sort(); //對陣列元素排序,返回陣列地址 |
8、 陣列元素的字串化
1 2 3 |
array.join(separator); //返回字串,這個字串將陣列的每一個元素值連線在一起,中間用 separator 隔開。 toLocaleString 、toString 、valueOf:可以看作是join的特殊用法,不常用 |
簡單介紹了下陣列各個方法的使用,也算是對js陣列學習的一個review和總結,利用這些方法可以實現陣列更復雜些的操作,具體大家可以自己去實踐。可見,js陣列的功能很強大。
二、 陣列屬性
1、 length屬性
length屬性表示陣列的長度,即其中元素的個數。因為陣列的索引總是由0開始,所以一個陣列的上下限分別是:0和length-1。和其他大多數語言不同的是,JavaScript陣列的length屬性是可變的,這一點需要特別注意。當length屬性被設定得更大時,整個陣列的狀態事實上不會發生變化,僅僅是length屬性變大;當length屬性被設定得比原來小時,則原先陣列中索引大於或等於length的元素的值全部被丟失。下面是演示改變length屬性的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var arr=[12,23,5,3,25,98,76,54,56,76]; //定義了一個包含10個數字的陣列 print(arr.length); //顯示陣列的長度10 arr.length=12; //增大陣列的長度 print(arr.length); //顯示陣列的長度已經變為12 print(arr[8]); //顯示第9個元素的值,為56 arr.length=5; //將陣列的長度減少到5,索引等於或超過5的元素被丟棄 print(arr[8]); //顯示第9個元素已經變為"undefined" arr.length=10; //將陣列長度恢復為10 print(arr[8]); //雖然長度被恢復為10,但第9個元素卻無法收回,顯示"undefined" |
由上面的程式碼我們可以清楚的看到length屬性的性質。但length物件不僅可以顯式的設定,它也有可能被隱式修改。JavaScript中可以使用一個未宣告過的變數,同樣,也可以使用一個未定義的陣列元素(指索引超過或等於length的元素),這時,length屬性的值將被設定為所使用元素索引的值加1。例如下面的程式碼:
1 2 3 4 5 6 7 |
var arr=[12,23,5,3,25,98,76,54,56,76]; print(arr.length); // 10 arr[15]=34; print(arr.length); // 16 |
程式碼中同樣是先定義了一個包含10個數字的陣列,可以看出其長度為10。隨後使用了索引為15的元素,將其賦值為15,即 arr[15]=34,這時再輸出陣列的長度,得到的是16。無論如何,對於習慣於強型別程式設計的開發人員來說,這是一個很令人驚訝的特性。事實上,使用new Array()形式建立的陣列,其初始長度就是為0,正是對其中未定義元素的操作,才使陣列的長度發生變化。
綜上,利用length屬性可以方便的增加或者減少陣列的容量。
2、 prototype屬性
返回物件型別原型的引用。prototype 屬性是 object 共有的。
objectName.prototype
objectName 引數是object物件的名稱。
對於陣列物件,以下例子說明 prototype 屬性的用途。
給陣列物件新增返回陣列中最大元素值的方法。要完成這一點,宣告一個函式,將它加入 Array.prototype, 並使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function array_max() { var i, max = this[0]; for (i = 1; i < this.length; i++) { if (max < this[i]) max = this[i]; } return max; } Array.prototype.max = array_max; var x = new Array(1, 2, 3, 4, 5, 6); print(x.max()); // 6 |
3、 constructor屬性
表示建立物件的函式。
object.constructor // object是物件或函式的名稱。
說明:constructor 屬性是所有具有 prototype 的物件的成員。constructor 屬性儲存了對構造特定物件例項的函式的引用。
1 2 3 |
x = new Array(); print(x.constructor === Array); // true |
JavaScript陣列演算法的C語言實現
使用沒有指標的語言,個人覺得無法將資料結構和演算法的精髓講的出來,而且js底層已將陣列相關演算法封裝好,所以這裡不使用原生的js或者java等,而是使用c語言來實現。為了照顧沒有學過指標的同學,我會盡可能的簡單實現,並寫好註釋,畫好圖解,大家可以體會一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# include <stdio.h> # include <malloc.h> //包含了malloc函式 # include <stdlib.h> //包含了exit函式 //定義了一個資料型別,該資料型別的名字叫做struct Arr, 該資料型別含有三個成員,分別是pBase, len, cnt struct Arr { int * pBase; //儲存的是陣列第一個元素的地址 int len; //陣列所能容納的最大元素的個數 int cnt; //當前陣列有效元素的個數 }; void init_arr(struct Arr *, int); //初始化陣列 bool is_empty(struct Arr *); // 陣列是否為空 bool is_full(struct Arr *); // 陣列是否已滿 bool push(struct Arr *, int); //追加元素 void sort(struct Arr *); // 排序 void reverse(struct Arr *); // 逆序 bool insert(struct Arr *, int, int); // 插入元素 bool del(struct Arr *, int, int *); // 刪除元素 void show_arr(struct Arr *); // 列印陣列 int main(void) { struct Arr arr; int val; // 儲存刪除元素 init_arr(&arr, 6); // 初始化陣列 show_arr(&arr); push(&arr, 4); // 在尾部追加元素 push(&arr, 1); push(&arr, -1); push(&arr, 10); push(&arr, 0); push(&arr, 6); show_arr(&arr); sort(&arr); // 排序 show_arr(&arr); reverse(&arr); // 逆序 show_arr(&arr); del(&arr, 4, &val); // 刪除指定位置元素 printf("您刪除的元素是: %d\n", val); show_arr(&arr); insert(&arr, 4, 20); // 在指定位置插入元素 show_arr(&arr); return 0; } void init_arr(struct Arr * pArr, int length) { pArr->pBase = (int *)malloc(sizeof(int) * length); if(NULL == pArr->pBase) { printf("動態記憶體分配失敗!\n"); exit(-1); //終止整個程式 } else { pArr->len = length; pArr->cnt = 0; } return; } bool is_empty(struct Arr * pArr) { if(0 == pArr->cnt) { return true; } else { return false; } } bool is_full(struct Arr * pArr) { if (pArr->cnt == pArr->len) { return true; } else { return false; } } void show_arr(struct Arr * pArr) { if(is_empty(pArr)) { printf("陣列為空!\n"); } else { for(int i=0; i<pArr->cnt; ++i) { printf("%d ", pArr->pBase[i]); } printf("\n"); } } bool push(struct Arr * pArr, int val) { //滿了就返回false if(is_full(pArr)) { return false; } //不滿時追加 pArr->pBase[pArr->cnt] = val; (pArr->cnt)++; return true; } void sort(struct Arr * pArr) { int i, j, t; // 簡單的氣泡排序法實現,後面的章節會單獨講排序演算法 for(i=0; i<pArr->cnt; ++i) { for(j=i+1; j<pArr->cnt; ++j) { if(pArr->pBase[i] > pArr->pBase[j]) { t = pArr->pBase[i]; pArr->pBase[i] = pArr->pBase[j]; pArr->pBase[j] = t; } } } } void reverse(struct Arr * pArr) { int i = 0; int j = pArr->cnt-1; int t; // 當i<j時,置換i和j位置的元素 while(i < j) { t = pArr->pBase[i]; pArr->pBase[i] = pArr->pBase[j]; pArr->pBase[j] = t; ++i; --j; } return; } bool insert(struct Arr * pArr, int pos, int val) { int i; // 滿了就算了 if(is_full(pArr)) { return false; } // 如果插入的位置不在陣列有效範圍內就算了 if(pos<1 || pos>pArr->cnt+1) { return false; } // 從插入位置開始後移各元素,將插入位置空出 for(i=pArr->cnt-1; i>=pos-1; --i) { pArr->pBase[i+1] = pArr->pBase[i]; } // 給插入位置的元素賦值 pArr->pBase[pos-1] = val; //陣列有效長度自增 (pArr->cnt)++; return true; } bool del(struct Arr * pArr, int pos, int * pVal) { int i; // 空就算了 if(is_empty(pArr)) { return false; } // 不在有效範圍內就算了 if (pos<1 || pos>pArr->cnt) { return false; } // 儲存被刪除元素 *pVal = pArr->pBase[pos-1]; // 從刪除位置開始,前移各元素,將刪除位置堵死 for (i=pos; i<pArr->cnt; ++i) { pArr->pBase[i-1] = pArr->pBase[i]; } // 陣列有效長度自減 pArr->cnt--; return true; } |
執行結果:
程式圖解:
衡量演算法的標準
需要詳細瞭解的同學請閱讀相關書籍。這裡我簡單介紹一下。
1、 時間複雜度
程式大概要執行的次數,而非執行的時間
通常使用大O表示法(含義:”order of”大約是)來表示。比如無序陣列的插入,無論陣列中有多少資料項,都只需要在下一個有空的地方進行一步插入操作,那麼可以說向一個無序陣列中插入一個資料項的時間T是一個常數K: T=K;又比如線性查詢,查詢特定資料項所需的比較次數平均為資料項總數的一半,因此可以說:T=KN/2,為了得到更加簡潔的公式,可以將2併入K,可以得到:T=KN。大O表示法同上面的公式比較類似,但是它省略了常數K。當比較演算法時,並不在乎具體的處理器或者編譯器,真正需要比較的是對應不同的N值T是怎樣變化的,而不是具體的數字。
用大O表示法表示陣列相關演算法執行時間:
演算法 | 大O表示法 |
---|---|
線性查詢 | O(N) |
二分查詢 | O(logN) |
無序陣列的插入 | O(1) |
有序陣列的插入 | O(N) |
無序陣列的刪除 | O(N) |
有序陣列的刪除 | O(N) |
注:O(1)是優秀;O(logN)是良好;O(N)還可以;O(N2)就差一些了。
2、 空間複雜度
演算法執行過程中大概所佔用的最大記憶體
3、 難易程度
寫出來的演算法不能只讓自己看得懂,或者自己寫完以後自己也看不懂了。。。
4、 健壯性
不能一用就崩潰。。。
為什麼不用陣列表示一切
僅用陣列看似可以完成所有的工作,那麼為什麼不用它來進行所有的資料儲存呢?
在一個無序陣列中可以很快進行插入(O(1)),但是查詢卻要花費較多的時間O(N)。在一個有序陣列中可以查詢的很快(O(logN)),但是插入卻要O(N)。對於有序和無序陣列,由於平均半數的資料項需要移動,所以刪除操作平均需要花費O(N)。
如果有一種資料結構進行任何插入、刪除和查詢操作都很快(O(1)或者O(logN)),那就太爽了哈。後面我們會向這一目標靠近。