數據結構—包(Bag)


  數據結構中的包,其實是對現實中的包的一種抽象。 想像一下現實中的包,比如書包,它能做什么?有哪些功能?首先它用來裝東西,里面的東西可以隨便放,沒有規律,沒有順序,當然,可以放多個相同的東西。其次,東西可以拿出來,拿出來也有幾種情況,隨便拿出一個,拿出特定的一個,比如書本,把所有的東西都拿出來。附帶的功能就是,包有沒有滿,包是不是空的,里面有多少東西,都是什么,里面是不是有書,放了幾本書之類的。想好之后,就可以定義數據結構中的包的方法了。

  裝東西,定義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;
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM