數據結構中的包,其實是對現實中的包的一種抽象。 想像一下現實中的包,比如書包,它能做什么?有哪些功能?首先它用來裝東西,里面的東西可以隨便放,沒有規律,沒有順序,當然,可以放多個相同的東西。其次,東西可以拿出來,拿出來也有幾種情況,隨便拿出一個,拿出特定的一個,比如書本,把所有的東西都拿出來。附帶的功能就是,包有沒有滿,包是不是空的,里面有多少東西,都是什么,里面是不是有書,放了幾本書之類的。想好之后,就可以定義數據結構中的包的方法了。
裝東西,定義add()方法,要接受一個參數,要裝的東西,沒有返回值。
隨便拿出一個東西,定義remove()方法, 沒有參數,返回拿出的東西。
拿出一個特定的東西,定義remove(anEntry), 接收一個參數,要拿出的東西,返回值是布爾值,表示有沒有成功,因為要拿出的東西,包里可能沒有
拿出所有東西,定義clear()方法,沒有參數,也沒有返回值。
包里有多少東西,定義getCurrentSize()方法,返回包中元素的個數,沒有參數。
包是不是空的,定義isEmpty()方法,沒有參數,返回布爾值。
包里有沒有書,定義contains()方法,它接受一個參數,要找的東西,返回布爾值。
包里有幾本書,定義getFrequencyOf()方法,接受一個參數,要找的東西,返回整數。
包里都是什么,可以定義toArray()方法,返回一個數組,包含包里所有的東西,也是定義迭代方法。
/** * 一個用來描述包的操作的接口 */ public interface BagInterface<T> { /** * 獲取包中元素的數量 * * @return 元素數量 */ int getCurrentSize(); /** * 包是否為空 * * @return 包為空,返回true, 否則返回false */ boolean isEmpty(); /** * 向包里添加一個元素 * * @param newEntry 要添加到包里的元素 */ void add(T newEntry); /** * 從包里刪除任意一個元素 * @return 刪除的元素 */ T remove(); /** * 從包里刪除一個給定的元素 * @param anEntry 要刪除的元素 * @return 是否刪除成功 */ boolean remove(T anEntry); /** * 刪除包中所有元素 */ public void clear(); /** * 計算一個給定元素的數量 * @param anEntry 給定的元素 * @return 給定元素的數量 */ public int getFrequencyOf(T anEntry); /** * 是否包含給定的元素 * @param anEntry 要查找的元素 * @return 如查包含返回true, 否則返回false */ public boolean contains(T anEntry); /** * 獲取包中所有的元素, * @return 包含包中所有元素新數組。 * 注意,如果包為空,返回空數組 */ T[] toArray(); }
使用數組實現bag --- 創建ArrayBag<T>類來實現BagInterface<T>
首先考慮類的屬性。既然決定用數組實現的,屬性中肯定有一個數組的引用。除此之外,還要有個屬性記錄包中元素的個數,因為要判斷包是否為空等。
private T[] bag; private int numberOfEntries; private static final int DEFAULT_CAPACITY = 25;
構造函數,初始化包。首先要創建數組對象,賦值給bag屬性,因為屬性中的bag只是一個數組的引用,並沒有真正地創建數組。在Java中,創建數組需要指定長度。數組的長度。既也可以讓使用者來決定,也可以提供一個默認容量,因此提供無參和有參兩個構造函數。其次,初始化時,包中並沒有元素,numberOfEntries初始化為0. 但怎么創建數組呢?假設構造函數接收一個參數capacity, bag = new T[capacity]; 不行。bag = new Object[capacity]; 還是不行。bag = (T[])new Object[capacity]; 倒是沒有報錯,但有warning(unchecked cast). 編譯器想讓你確保數組中的每一個元素從Object類強制轉化成泛型T是安全的,由於數組剛剛創建,每一個元素都是null,因此轉化是安全的,我們可以使用@SuppressWarnings("unchecked")告訴編譯器忽略這個warning。@SuppressWarnings("unchecked")只能出現在方法定義或變量聲明之前,由於bag = (T[])new Object[capacity]; 是賦值操作,不是變量聲明,因為bag已經聲明了,最終創建數組如下
// 強制類型轉化是安全的,因為新數組中所有元素都是null @SuppressWarnings("unchecked") T[] tempBag = (T[])new Object[capacity]; // Unchecked cast bag = tempBag;
整個構造函數如下
/** * 創建一個空bag,初始空量為用戶指定容量 * @param capacity 指定容量 */ public ArrayBag(int capacity) { // 強制類型轉化是安全的,因為新數組中所有元素都是null @SuppressWarnings("unchecked") T[] tempBag = (T[]) new Object[capacity]; // Unchecked cast bag = tempBag; numberOfEntries = 0; } /** * 創建一個空bag,初始空量為默認容量25 */ public ArrayBag() { this(DEFAULT_CAPACITY); }
先實現add()方法,只有添加了元素,其它方法才好實現或測試。添加元素時,如果數組滿了,肯定不能添加了,需要擴容(增加容量),再添加。如果數組沒有滿,就可以繼續添加。擴容后面再說,先看數組沒有滿的情況。添加元素,就是把要添加的元素直接放到數組中最后一個元素的后面,元素個數加1。剛開始時,數組為空,numberOfEntries為0,數組中沒有元素,直接在0位置放置新元素,然后numberOfEntries + 1。再添加一個元素,那就要放到1的位置,numberOfEntries+1。再添加一個元素,那就要放到2的位置,numberOfEntries+1。
你會發現,新元素的放置位置就是bag[numberOfEntries]的位置,add()方法就是
public void add(T newEntry) { if(isArrayFull()){ } else { bag[numberOfEntries] = newEntry; numberOfEntries++; } }
isArrayFull()就是判斷數組是不是滿了,只要元素的個數等於數組的長度就是滿了
private boolean isArrayFull() { return numberOfEntries == bag.length; }
添加方法實現完了,就要看看實現的對不對,添加的元素有沒有添加成功。這就是ToArray() 方法了,創建一個新數組,把bag中的元素復制過去,然后把新數組返回
public T[] toArray() { @SuppressWarnings("unchecked") T[] result = (T[])new Object[numberOfEntries]; // Unchecked cast for (int index = 0; index < numberOfEntries; index++) { result[index] = bag[index]; } return result; }
添加和查看成功后,就要實現其它方法了。先從簡單的開始,isEmpty(),包是否為空,直接判斷numberOfEntries是否等於0就可以了。 getCurrentSize(), 包中元素的個數,直接返回numberofEntries。
public boolean isEmpty() { return numberOfEntries == 0; } public int getCurrentSize() { return numberOfEntries; }
getFrequencyOf(T anEntry),一個元素在包中出現的次數。循環遍歷數組就可以了,在遍歷過程中,只要有元素和要查找的元素相等,計數器加1。遍歷完成后,返回計數器。
public int getFrequencyOf(T anEntry) { int count = 0; for (int i = 0; i < numberOfEntries; i++) { if(anEntry.equals(bag[i])){ count++; } } return count; }
contains()方法,包中是否包含某個元素,還是循環遍歷數組,只不過是返回true或false。可以先設一個表示找到找不到的變量found,默認是false,只有當fasle的時候,才遍歷數組,在遍歷過程中,如果找到了,設為true。如果遍歷完,還沒有找到,那還是false,直接返回found就可以了。
public boolean contains(T anEntry) { boolean found = false; int index = 0; while (!found && index < numberOfEntries){ if (anEntry.equals(bag[index])){ found = true; } index++; } return found; }
clear()方法,清空bag,簡單一點的實現,就是numberOfEntries = 0;,復雜一點就是包不為空的時候,循環調用remove()方法
public void clear() { // numberOfEntries = 0; while (!isEmpty()){ remove(); } }
remove() 方法,刪除任意一個元素,由於包中的元素沒有順序要求,可以隨便刪除,簡單起見,就刪除最后一個元素,當然,如果包為空的話,是不允許刪除的,可以拋出錯誤。
public T remove() { if (isEmpty()) { throw new RuntimeException("空"); } T result = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return result; }
remove(anEntry),刪除一個給定的元素,首先要先查找這個元素,如果包中沒有這個元素,也就沒有辦法刪除,直接return false就好了。如果找到了,再想辦法刪除。查找,用的是循環遍歷,找到了,也就是找到了這個元素所在的位置。
怎么刪除呢?最先想到的是把后面的元素向前移,因為數組是連續的。
有點復雜。因為bag中的元素,並沒有規定順序,也就沒有必要前移。可以讓要刪除的元素和最后一個元素,進行交換,直接刪除最后一個元素就好了。
public boolean remove(T anEntry) { if(isEmpty()){ throw new RuntimeException("空"); } boolean found = false; int index = 0; while (!found && index < numberOfEntries){ if (anEntry.equals(bag[index])){ found = true; } else { index++; } } if(found){ bag[index] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return true; } else { return false; } }
remove()和remove(T anEntry)代碼有了重復,實現上remove() 就是remove(numberOfEntry-1), 這時可以寫一個私有方法,刪除給定index處的元素,它接受一個index參數,返回要刪除的元素
private T removeEntry(int givenIndex){ if(isEmpty()){ throw new RuntimeException("空"); } T result = bag[givenIndex]; bag[givenIndex] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return result; }
remove()方法簡化成
public T remove() { return removeEntry(numberOfEntries - 1); }
再看remove(T anEntry), 里面查找定位元素的代碼和contains()方法,也是重復的,也可以寫一個私有方法getIndexOf來返回index。
private int getIndexOf(T anEntry) { int where = -1; boolean found = false; int index = 0; while (!found && (index < numberOfEntries)) { if (anEntry.equals(bag[index])) { found = true; where = index; } index++; } return where; }
contains()方法就變成了
public boolean contains(T anEntry) { return getIndexOf(anEntry) > -1; }
remove(T anEntry)變成了
public boolean remove(T anEntry) { int index = getIndexOf(anEntry); if(index > -1){ removeEntry(index); return true; } else { return false; } }
數組擴容的基本原理:假設有一個數組myArray,先讓它賦值一個臨時變量oldArray, 再創建一個新數組賦值給myArray, 最后把oldArray中的每一個元素都復制到myArray中。
擴容要注意是新生成的數組的大小。如果太小,就要經常擴容,也就是經常把元素從一個數組復制到另一個數組,浪費性能。通常來說,新的數組長度是舊的數組的2倍。比如有50個元素,添加51個元素的時候,擴容到100,剩下的49個元素添加,就不用擴容,抵消掉這個擴容的成本。expanding the array by m elements spreads the copying cost over m additions instead of just one. Doubling the size of an array each time it becomes full is a typical approach. When increasing the size of an array, you copy its entries to a larger array. You should expand the array sufficiently to reduce the impact of the cost of copying. A common practice is to double the size of the array.
private void resize(int newLength) { @SuppressWarnings("unchecked") T[] newArray = (T[]) new Object[newLength]; // Unchecked cast for (int index = 0; index < numberOfEntries; index++) { newArray[index] = bag[index]; } bag = newArray; }
add()方法變成了
public void add(T newEntry) { if (isArrayFull()) { resize(2 * bag.length); } bag[numberOfEntries] = newEntry; numberOfEntries++; }
remove方法需要縮容(減少容量),如果把所有元素都刪除了,剩下那么大空間也不合適。縮容和擴容的原理相似,只不過是創建的新數組比原數組要小。縮容的時機是,當刪除的元素時,數組元素的個數是數組長度的1/4時,縮小一半的容量。removeEntry() 方法改成
private T removeEntry(int givenIndex){ if(isEmpty()){ throw new RuntimeException("空"); } T result = bag[givenIndex]; bag[givenIndex] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; if(numberOfEntries > 0 && numberOfEntries == bag.length / 4){ resize(bag.length / 2); } return result; }
使用鏈表實現bag --- 創建LinkedListBag<T>類來實現BagInterface<T>
還是先考慮類的屬性。鏈表是由節點組成,節點中的數據域可以存放包中的元素
需要創建一個私有內部類Node
private class Node { T data; Node next; Node(T dataPortion) { data = dataPortion; next = null; } }
操作鏈表需要頭指針,它指向鏈表中的第一個節點,還需要一個變量來記錄包中元素的個數。
private Node firstNode; private int numberOfEntries;
add(T newEntry) 方法,既然包中的元素沒有順序,而在鏈表的頭部插入節點,又比較簡單,那add(T newEntry) 就定義為從頭部插入節點。剛開始,鏈表為空,插入節點,就是創建新節點,並賦值給頭指針
Node newNode = new Node(newEntry); firstNode = newNode;
當鏈表不為空時,頭部插入節點,就是,創建新節點,新節點的next指向鏈表中的第一個節點(firstNode),再讓新節點成為鏈表中的第一個節點(新節點賦值給firstNode)
Node newNode = new Node(newEntry); newNode.next = firstNode; firstNode = newNode; // New node is at beginning of chain
實際上,向一個空鏈表中插入節點,和向一個非空鏈表頭部插入節點是一樣的。向空鏈表中插入節點時,如果加入newNode.next = firstNode,也沒有問題,因為此時firstNode為null,而newNode的next本來也是null。所以add()方法的完整實現
public void add(T newEntry) { Node newNode = new Node(newEntry); newNode.next = firstNode; firstNode = newNode; numberOfEntries++; }
toArray()方法,需要遍歷鏈表,把鏈表中的每一個節點中的數據放到數組中。怎么遍歷呢?firstNode指向鏈表中的第一個節點,第一個節點又包含第二個節點的引用,第二個節點又包含第三個節點的引用,因此需要一個變量來順序地引用每一節點,到達每一個節點時, .data就可以獲取節點中的數據。剛開始的時候,變量引用第一個節點,把firstNode賦值給這個變量,假設變量是currrentNode, 那么currentNode = firstNode. currentNode.data就可以獲取到數據。currentNode= currentNode.next, currentNode指向第二個節點,currentNode.data獲取數據。currentNode = currentNode.next,第三個節點,一直到最后一個節點currentNode為null。
public T[] toArray() { @SuppressWarnings("unchecked") T[] array = (T[]) new Object[numberOfEntries]; Node current = firstNode; int index = 0; while (current != null){ array[index] = current.data; index++; current = current.next; } return array; }
getFrequencyOf(), 像toArray()一樣,遍歷鏈表,只不過獲取到數據后,要做的是判斷是否相等。
public int getFrequencyOf(T anEntry) { int frequency = 0; Node currentNode = firstNode; while (currentNode != null) { if (anEntry.equals(currentNode.data)) frequency++; currentNode = currentNode.next; } return frequency; }
contains() 還是遍歷鏈表
public boolean contains(T anEntry){ boolean found = false; Node currentNode = firstNode; while (!found && (currentNode != null)) { if (anEntry.equals(currentNode.data)) found = true; else currentNode = currentNode.next; } return found; }
remove(), 因為包中的元素順序沒有要求,所以刪除第一個元素就好了,簡單
public T remove() { T result = null; if (firstNode != null) { result = firstNode.data; firstNode = firstNode.next; numberOfEntries--; } return result; }
remove(T anEntry), 遍歷鏈表,找到元素,然后和第一個節點進行交換,刪除第一個節點
private Node getReferenceTo(T anEntry) { boolean found = false; Node currentNode = firstNode; while (!found && (currentNode != null)) { if (anEntry.equals(currentNode.data)) found = true; else currentNode = currentNode.next; } return currentNode; } public boolean remove(T anEntry) { boolean result = false; Node nodeN = getReferenceTo(anEntry); if (nodeN != null) { nodeN.data = firstNode.data; firstNode = firstNode.next; numberOfEntries--; result = true; } return result; }
clear()方法,直接讓firstNode = null就好了
public void clear() { firstNode = null; }
迭代方法,就要實現Iterable<T>接口,實現Iterable<T>接口,就要提供一個iterator方法,這個方法要返回Iterator<T>接口類型的對象,有對象就要創建一個類來這個Iterator<T>接口,它有兩個方法,一個是hasNext(),表示,還有迭代對象中還沒有元素,next()方法,返回每一次迭代的元素。
import java.util.Iterator; public class LinkedListBag<T> implements BagInterface<T>, Iterable<T> { public Iterator<T> iterator() { return new ListIterator(); } private class ListIterator implements Iterator<T> { private Node current = firstNode; public boolean hasNext() { return current != null; } public T next() { T item = current.data; current = current.next; return item; } } }