數(shù)據(jù)結(jié)構(gòu)與算法--線性表的順序存儲(chǔ)結(jié)構(gòu)
線性表是一個(gè)序列,可以想象成一群有先后順序的的元素集合。線性表是有限序列,所以存在一個(gè)開(kāi)頭和結(jié)尾。開(kāi)頭的元素有且只有一個(gè)后繼,結(jié)尾的元素有且只有一個(gè)前驅(qū)。中間的元素分別有一個(gè)前驅(qū)和后繼。每個(gè)元素都清楚它們的前驅(qū)和后繼是哪個(gè)元素,因此形成了順序。稍微復(fù)雜的線性表,每個(gè)元素元素可以由多個(gè)數(shù)據(jù)項(xiàng)組成。比如鏈表的Node,Node是一個(gè)數(shù)據(jù)元素,每個(gè)Node都含有data
(存放的數(shù)據(jù))和next
(指向下一元素的指針)組成,雙向鏈表還有prev
(指向上一個(gè)元素的指針)。
線性表中存儲(chǔ)的元素類(lèi)型一般相同。比如ArrayList<String>
里就只能存String,而且通常我們處理的序列它們?cè)仡?lèi)型相同的情況居多。為此需要引用泛型,比如用Item
表示某一不確定的類(lèi)型,但是在之后的處理中,只接受Item或者Item的子類(lèi)(多態(tài))。這樣就保證了我們存入的元素是同一類(lèi)型的。
線性表的順序存儲(chǔ)結(jié)構(gòu)
這里說(shuō)的順序,指的是內(nèi)存空間上的順序,即劃分出一塊連續(xù)的空間,就好比售票口開(kāi)設(shè)了一個(gè)窗口。人們排成一列;而且要求數(shù)據(jù)元素類(lèi)型一致(放一本書(shū)在那兒替你排隊(duì),自己跑去瀟灑,誰(shuí)都不會(huì)同意)。這樣的數(shù)據(jù)存儲(chǔ)方式我們很容易就想到一維數(shù)組。Java中數(shù)組的初始化必須指定長(zhǎng)度。指定多大的長(zhǎng)度呢?由于數(shù)據(jù)量未知,指定容量過(guò)大就浪費(fèi)空間,容量小了又裝不了多少元素。在剛開(kāi)始初始化時(shí)就難住我們了。有一種做法是,如果能估計(jì)數(shù)據(jù)量,那么指定一個(gè)最大容量比估計(jì)的數(shù)據(jù)量適當(dāng)大些,留點(diǎn)余地。最大容量在初始化時(shí)指定,如String[] = new String[500]
,指定了最多裝500個(gè)元素。用編程語(yǔ)言表達(dá)即是capacity = 500
;至于究竟裝了多少個(gè),用size
表示實(shí)際存入的元素個(gè)數(shù)。顯然size <= capacity
。還有一種做法比較懶,不用操心數(shù)組裝不下的問(wèn)題:等到不夠了,我立馬增大容量;等到空閑的空間太多了,我立馬釋放一些。由于常常我們面對(duì)的數(shù)據(jù)量未知,這不失為一種好方法。
順序存儲(chǔ)結(jié)構(gòu),關(guān)鍵是劃分一塊連續(xù)的地址空間,連續(xù)意味著什么?假如一個(gè)序列從a0到an,每個(gè)元素占據(jù)c個(gè)存儲(chǔ)單元。如果我知道了任何一個(gè)元素的地址,比如a3處,其在內(nèi)存中的位置是locate(a3)。那么a8在內(nèi)存中的位置可以立刻算出為locate(a8) = locate(a3) + (8-3)c
,更一般地locate(ai) = locate(a0) + ic
。這說(shuō)明我只需定位第一個(gè)元素,后面的元素的位置實(shí)際上已經(jīng)固定下來(lái)了。因?yàn)檫@樣的結(jié)構(gòu),我們?cè)L問(wèn)a[0]和a[5]或者a[n]是一樣復(fù)雜的(或者應(yīng)該叫簡(jiǎn)單)。訪問(wèn)a[i] (0 <= i <= size -1)
的時(shí)間復(fù)雜度都是O(1)。
順序存儲(chǔ)結(jié)構(gòu)的特點(diǎn)
想象一個(gè)例子,和你住一條街的鄰居。以自家為a[0],你可以輕松說(shuō)出西邊第3家a[3]是誰(shuí)家,你也能直接定位到西邊第9家a[9]是誰(shuí)家——記憶方式是第i家姓a[i]。如果是這樣記憶的,那么隨便給一個(gè)數(shù)字,你就能脫口而出那個(gè)位置是誰(shuí)家。訪問(wèn)a[i]時(shí)間復(fù)雜度為O(1)。
如果新搬來(lái)一家,最終落戶到你家旁邊,成為你家最新的鄰居(隔開(kāi)了你和你原來(lái)的鄰居),別人在問(wèn)你第5家是誰(shuí)家時(shí),你心里想新來(lái)了一家,原來(lái)的第五家已經(jīng)成為了第六家。實(shí)際上從第一家開(kāi)始,你之前所有的記憶都不適用了,后面的所有位置都變化了,你需要重新記憶一次,如果時(shí)不時(shí)搬來(lái)一家,腦袋可不爆炸了!有人搬家走了也類(lèi)似。插入或者移除在最壞情況下所有元素都要移動(dòng)一次,所以時(shí)間復(fù)雜度為O(n)。
線性表--順序存儲(chǔ)結(jié)構(gòu)的實(shí)現(xiàn)
為了接收多種類(lèi)型,就像ArrayList那樣。使用了泛型,在下面的代碼中用Item表示。同時(shí)實(shí)現(xiàn)了Iterable<Item>
使得該類(lèi)是可迭代的,能使用for-each語(yǔ)句。實(shí)現(xiàn)Iterable接口,必須實(shí)現(xiàn)它的iterator()
方法,該方法返回一個(gè)Iterator
對(duì)象,我使用了匿名內(nèi)部類(lèi)的方式,且實(shí)現(xiàn)Iterator接口的hasNext
和next
方法。
由于使用到了一維數(shù)組,而且類(lèi)型為泛型Item,我們知道Java中不能直接創(chuàng)捷泛型數(shù)組。下面的寫(xiě)法是錯(cuò)誤的
Item[] a = new Item[capacity];
必須先創(chuàng)建Object數(shù)組,再向下轉(zhuǎn)型為Item類(lèi)型。
Item[] a = (Item[]) new Object[capacity];
另外,該數(shù)組是可調(diào)節(jié)容量的,表長(zhǎng)度即將超過(guò)容量時(shí),自動(dòng)增大;表長(zhǎng)度容量遠(yuǎn)小于容量時(shí),容量減小。
好,先上全部代碼,然后慢慢解釋。
package Chap3;
import java.util.Iterator;
// 實(shí)現(xiàn)Iterable為了使用for-each語(yǔ)句,同時(shí)要實(shí)現(xiàn)iterator方法
public class LinearList<Item> implements Iterable<Item> {
private int N;
// 初始化為長(zhǎng)度為1,方便第一次add的時(shí)候可以訪問(wèn)a[0]這個(gè)下標(biāo)
private Item[] a = (Item[]) new Object[1];
public LinearList(Item... items) {
for (int i = 0; i < items.length; i++) {
add(items[i]);
}
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public Item get(int index) {
checkRange(index);
return a[index];
}
public void set(int index, Item item) {
checkRange(index);
a[index] = item;
}
// 先判斷是不是沒(méi)有容量了,若不先增容,會(huì)越界。移位從最后一個(gè)元素開(kāi)始,仔細(xì)想想為什么
public void insert(int index, Item item) {
checkRangeForInsert(index);
if (N == a.length) {
resize(2 * a.length);
}
for (int k = N - 1; k >= index; k--) {
a[k + 1] = a[k];
}
a[index] = item;
N++;
}
// 移除之后再檢查是否長(zhǎng)度太小需要節(jié)約空間,否則先縮小的話,可能導(dǎo)致訪問(wèn)時(shí)越界
public Item remove(int index) {
checkRange(index);
Item item = a[index];
// 這里就需要正向遍歷了
for (int k = index; k < N - 1; k++) {
a[k] = a[k + 1];
}
a[N - 1] = null;
N--;
if (N > 0 && N == a.length / 4) {
resize(a.length / 2);
}
return item;
}
// 先判斷是不是沒(méi)有容量了,若不先增容,會(huì)越界
public void add(Item item) {
if (N == a.length) {
resize(2 * a.length);
}
a[N++] = item;
}
public int indexOf(Item item) {
if (item != null) {
for (int i = 0; i < N; i++) {
if (item.equals(a[i])) {
return i;
}
}
} else {
for (int i = 0; i < N; i++) {
if (a[i] == null) {
return i;
}
}
}
return -1;
}
public boolean contains(Item item) {
return indexOf(item) >= 0;
}
// N=0但是a.length不為0,可以再次add
public void clear() {
for (int i = 0; i < N; i++) {
a[i] = null;
}
N = 0;
}
private void resize(int max) {
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++) {
temp[i] = a[i];
}
// 將容量大于N的數(shù)組傳給a
a = temp;
}
// 檢查數(shù)組下標(biāo)是否越界,注意是N而不是a.length, 因?yàn)閍的容量比N大,訪問(wèn)N之后的也不會(huì)觸發(fā)異常
// insert的時(shí)候允許向a[N]處插入,這里==N不會(huì)拋出異常
private void checkRangeForInsert(int index) {
if (index > N || index < 0) {
throw new IndexOutOfBoundsException(index + "");
}
}
// 其他情況如remove就不能訪問(wèn)a[N]了
private void checkRange(int index) {
if (index >= N || index < 0) {
throw new IndexOutOfBoundsException(index + "");
}
}
@Override
public Iterator<Item> iterator() {
return new Iterator<Item>() {
private int i = 0;
@Override
public boolean hasNext() {
return i < N;
}
@Override
public Item next() {
return a[i++];
}
};
}
@Override
public String toString() {
Iterator<Item> it = iterator();
if (! it.hasNext()) {
return "[]";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
while (true){
Item item = it.next();
sb.append(item);
if (! it.hasNext()) {
return sb.append("]").toString();
}
sb.append(", ");
}
}
public static void main(String[] args) {
LinearList<String> b = new LinearList<>();
b.add("god");
b.add("yes");
b.add("no");
b.add("man");
b.insert(0, "ffff");
System.out.println(b.remove(0)); // ffff
b.set(1, "ggg");
System.out.println(b.get(1)); // ggg
System.out.println(b.indexOf("no")); // 2
System.out.println(b.size()); // 4
/* now b have:
god
ggg
no
man
*/
System.out.println("*******");
LinearList<Integer> c = new LinearList<>(1, 2, 3, 4, 5);
System.out.println(c);
System.out.println(c.contains(5)); // true
c.clear();
c.add(66);
}
}
數(shù)組容量的調(diào)節(jié)由resize(int max)
方法處理。該方法的原理就是創(chuàng)建一個(gè)長(zhǎng)度為max的臨時(shí)數(shù)組,將原數(shù)組的所有數(shù)據(jù)復(fù)制到臨時(shí)數(shù)組,然后將臨時(shí)數(shù)組的引用傳給原數(shù)組。每次要新增元素時(shí),先檢查數(shù)組容量和表長(zhǎng)度是否相等,相等說(shuō)明已經(jīng)沒(méi)有空間存放新來(lái)的元素,故增大容量到原來(lái)的兩倍;類(lèi)似的,每移除一個(gè)元素后,再判斷表長(zhǎng)度是否只有容量的1/4了,若是就縮小容量到原來(lái)的一半。
private Item[] a = (Item[]) new Object[1]
之所以指定容量為1,第一是因?yàn)槿萘靠勺詣?dòng)調(diào)節(jié),無(wú)需指定得很大,當(dāng)然不能指定為0。因?yàn)榈谝淮翁砑釉貢r(shí),看add
方法一開(kāi)始N == a.length
就會(huì)執(zhí)行resize(2 * a.length);
如果初始化時(shí)容量設(shè)置為0,resize后還是0。
checkRange
方法用來(lái)判斷數(shù)組腳標(biāo)是否越界,訪問(wèn)的index在[0, N - 1]的范圍內(nèi)不會(huì)拋出異常。insert
方法中也檢查了數(shù)組腳標(biāo)。不過(guò)使用的是checkRangeForInsert(index)
和checkRange
不同的是,當(dāng)index為N時(shí)也不會(huì)拋出異常,因?yàn)槲覀冊(cè)试S在a[N]的位置插入元素,add方法
其實(shí)就是insert(N, item)
的簡(jiǎn)寫(xiě)。
接下來(lái)看關(guān)鍵方法insert和remove。
插入時(shí),從最后一個(gè)元素開(kāi)始,向后移動(dòng)一次,接下來(lái)倒數(shù)第二個(gè)元素向后移動(dòng)一次,直到插入點(diǎn)index處向后移動(dòng)一次,結(jié)束移動(dòng)。移動(dòng)的總次數(shù)為N - index
。現(xiàn)在插入點(diǎn)空著,在插入點(diǎn)安排新元素就OK了。注意必須是從最后一個(gè)元素開(kāi)始移動(dòng),如果從插入點(diǎn)開(kāi)始移動(dòng),就會(huì)占用別的元素的位置,導(dǎo)致混亂。記住一個(gè)原則:始終朝著空閑的地方移動(dòng)!
移除元素時(shí),也是類(lèi)似的。先彈出要移除的元素,現(xiàn)在這個(gè)位置空閑了,從該位置的下一個(gè)位置開(kāi)始向前移動(dòng)一次,直到最后一個(gè)元素向前移動(dòng)一次,結(jié)束移動(dòng)。移動(dòng)的總次數(shù)為N - index - 1
。
clear()
可以清空表的所有元素,其實(shí)就是將[0, N-1]范圍內(nèi)的所有元素置為null,再將長(zhǎng)度置為0。indexOf(Item item)
,可以查找item第一次出現(xiàn)的位置,也接受null(因?yàn)閍dd的時(shí)候可以添加null)。原理很簡(jiǎn)單,遍歷查找,找到了就返回當(dāng)前腳標(biāo)。contains(Item item)
用到了indexOf
,顯然返回的腳標(biāo)不為-1,說(shuō)明存在這個(gè)元素。
還重寫(xiě)了toString
方法,可以直接將對(duì)象以列表形式打印出來(lái)。
by @sunhaiyu
2017.7.30