自定義OrderedMap

lointo114000發表於2017-09-27

一、 自定義OrderedMap

在Go語言中,字典型別的元素值的迭代順序是不確定的。想要實現有固定順序的Map就需要讓自定義的 OrderedMap 實現 sort.Interface 介面型別。該介面型別中的方法 LenLessSwap 的含義分別是獲取元素的數量、比較相鄰元素的大小以及交換它們的位置。

1.定義 OrderedMap

想要實現自定義一個有序字典型別,僅僅基於字典型別是不夠的,需要使用一個元素有序的資料型別值作為輔助。如下宣告瞭一個名為 OrderedMap 的結構體型別:

type OrderedMap struct {
    keys    []interface{}
    m       map[interface{}]interface{}
}複製程式碼

如上在 OrderedMap 型別中,除了一個字典型別的欄位 m,還有一個切片型別的欄位。

2.實現 sort.Interface

這裡需要新增為 OrderedMap 型別如下方法:

// 獲取鍵值對的數量
func (omap *OrderedMap) Len() int {
    return len(omap.keys)
}

func (omap *OrderedMap) Less(i, j, int) bool {
    // 省略若干條語句
}

func (omap *OrderedMap) Swap(i, j int) {
    omap.keys[i], omap.keys[j] = omap.keys[j], omap.keys[i];
}
複製程式碼

如上 *OrderMap 型別(不是 OrderedMap 型別)就是一個 sort.Interface 介面型別的實現型別了。可以看出,Len 方法中是以 keys 欄位的值的長度作為結果值。而在 Swap 方法中,使用平行賦值語句交換的兩個元素值也是操作的 keys 欄位的值。

現在考慮 Less 方法的實現,方法 Less 的功能是比較相鄰的兩個元素值得大小病返回判斷結果。讀者如果瞭解Go語言值的可比性與有序性(詳細的各種資料型別的比較方法可以參考本人的Go語言學習筆記6中所述),就知道在Go語言中,只有當值的型別具備有序性的時候,它才可能與其他的同型別值比較大小。另外Go語言規定,字典型別的鍵型別的值必須是可比的(即可以判定兩個該型別的值是否相等)。但是,在Go語言中具有可比性的資料型別中只有一部分同時具備有序性,因此只是依靠Go語言本身對字典型別的鍵型別的約束是不夠的。在Go語言中,具備有序性的預定義資料型別只有整數型別浮點數型別字串型別

型別 OrderedMap 中的欄位 keys[]interface{} 型別的,因此想要有序性,總是需要比較兩個 []interface{} 型別值的大小。因為介面型別的值只具有可比性而不具備有序性,所以上面宣告的OrderedMap 型別在這裡顯得不適用了。如果將 keys 欄位的元素型別改為某一個具體的資料型別(整數型別、浮點數型別和字串型別),又會使得 OrderedMap 型別的使用很侷限。

因此這裡需要對 keys 欄位進行詳細的分析,如下需求列表:

  • 欄位keys的值中的元素值應該都是有序的,應該可以方便地比較它們之間的大小。
  • 欄位keys的值的元素型別不應該是一個具體的型別,應該可以在執行時再確定它的元素型別。
  • 欄位keys的值應該可以方便地進行新增元素值、刪除元素值以及獲取元素值等操作。
  • 欄位keys的值中的元素值應該可以被依照固定的順序獲取。
  • 欄位keys的值中的元素值應該能夠被自動地排序。
  • 欄位keys的值總是已排序的,應該能夠確定某一個元素值的具體位置。
  • 欄位keys既然可以在執行時決定它的值的元素型別,那麼就可以在執行時獲知這個元素型別。
  • 欄位keys的值中的不同元素值比較大小的具體方法,應該可以在執行時獲知到。

3.定義Keys介面型別

為了滿足上面的需求列表,可以定義如下的名為Keys的介面型別:

type Keys interface {
    sort.Interface
    Add(k interface{}) bool
    Remove(k interface{}) bool
    Clear()
    Get(index int) interface{}
    GetAll() []interface{}
    Search(k interface{})(index int, contains bool)
    ElemType() reflect.Type
    CompareFunc() func(interface{}, interface{}) int8
}複製程式碼

Keys 介面型別中嵌入了 sort.Interface 介面型別,也就是說 Keys 型別的值一定是可排序的。AddRemoveClearGet 這4個方法使得可以對 Keys 的值進行新增,刪除,清除和獲取元素值的操作。GetAll 方法可以獲取一個與 Keys 型別值有著相同元素值集合和元素迭代順序的切片值。Search 方法確定某一個元素值的具體位置,CompareFunc 返回一個比較大小的具體方法,ElemType方法返回一個 reflect.Type 型別的結果值。

注意:實際上,reflect 包中的程式實體提供了Go語言執行時的反射機制。通過這些程式實體,可以編寫出一些程式碼來動態的操縱任意型別的物件(如 TypeOf 函式用於獲取一個 interface{型別的值的動態型別資訊})。

4. Keys介面型別的實現型別

Keys 介面型別的定義並沒有體現需求列表中的第1 , 2 , 5項所描述的功能。既然 Keys 介面型別的值必須是 sort.Interface 介面的一個實體,通過 sort 程式碼包中的程式實體實現元素自動排序的功能應該不難。

為了能夠動態地決定元素型別,需要在這個 Keys 的實現型別中宣告一個 []interface{} 型別的欄位,以作為儲存被新增到 Keys 型別值中的元素值得底層資料結構:

Container []interface{}複製程式碼

另外由於Go語言本身並沒有對自定義泛型提供支援,因此需要這個欄位的值儲存某一個資料型別的元素值。但是介面型別的值不具備有序性(即不能比較大小)。儘管這樣,我們可以讓具體使用者去實現一個比較大小的方法,還需新增如下欄位:

compareFunc func(interface{}, interface{}) int8複製程式碼

這是一個函式型別的欄位,這個函式返回一個 int8 型別的結果值,對結果值做出如下規定:

當第一個引數值小於第二個引數值時,結果值應該小於0.
當第一個引數值大於第二個引數值時,結果值應該大於0
當第一個引數值等於第二個引數值時,結果值應該等於0

現在,通過將比較兩個元素值大小的問題拋給使用者,既解決了需要動態確定元素型別的問題,又明確了比較兩個元素值大小的解決方法。不過,由於 container 欄位是 []interface{} 型別的,常常不能夠方便地在執行時獲取到它的實際元素型別(比如在它的值中還沒有任何元素值的時候)。這裡需要一個明確 container 欄位的實際元素型別的欄位,這個欄位的值所代表的型別也應該是當前的Keys型別值的實際元素型別。如下 Keys 介面型別的實現型別的宣告如下:

type myKeys struct {
    container       []interface{}
    compareFunc     func(interface{}, interface{}) int8
    elemType        reflect.Type
}複製程式碼

現在使用一個 *myKeys 型別的值來儲存 int64 型別的元素值,應該如下來初始化它:

int64Keys := &myKeys{
    container : make([]interface{}, 0),
    compareFunc : func(e1 interface{}, e2 interface{}) int8 {
        k1 := e1.(int64)
        k2 := e2.(int64)
        if k1 < k2 {
            return -1
        } else if k1 > k2 {
            return 1
        } else {
            return 0
        }
    },
    elemType : reflect.Typeof(int64(1))
}複製程式碼

注意: compareFunc 欄位的值中的那兩個型別斷言表示式的目標型別一定要與 elemType 欄位的值所代表的型別保持一致。elemType 欄位的值所代表的型別其實就是呼叫 reflect.TypeOf 函式時傳入的那個引數值的型別,即 int64

被用於實現 sort.Interface 介面型別的方法的宣告如下:

func (keys *myKeys) Len() int {
    return len(keys.container)
}

//該方法中,比較兩個元素值的操作全權交給了compareFunc欄位所代表的那個函式
func (keys *myKeys) Less(i, j int)bool {
    return keys.compareFunc(keys.container[i], keys.container[j]) == -1
}

func (keys *myKeys) Swap(i, j int) {
    keys.container[i], keys.container[j] = keys.container[j], keys.container[i];
}複製程式碼

如上這3個方法的接收者型別都是 *myKeys,所以事先 sort.Interface 介面型別的型別是 *myKeys 而不是 myKeys

5.實現Add方法

現在考慮實現 Add 方法,但在真正向欄位 container 的值新增元素值之前,需要先判斷這個元素值的型別是否符合要求。當然,這需要使用欄位 elemType 的值,它代表了可接受的元素值的型別。現在使用一個獨立的方法來實現這個判斷,如下:

func (keys *myKeys) isAcceptableElem(k interface{}) bool {
    if k == nil {
        return false
    }
    if reflect.TypeOf(k) != keys.elemType {
        return false
    }
    return true
}複製程式碼

Add 方法中,使用 isAcceptableElem 方法來判定元素值的型別是否可被接收。如果結果是否定的,直接返回 false ;如果結果是肯定的,就向 container 欄位的值新增這個元素值。在新增之後,應該對 container 的值中的元素值進行一次排序。這需要用到 sort 程式碼包中的排序函式 sort.Sort,它的宣告如下:

func Sort(data Interface) {
    // 省略若干語句
}複製程式碼

函式 sort.Sort 的簽名中的引數型別 Interface 其實就是介面型別 sort.Interface,並且這兩個程式實體處在同一個程式碼包中。

知識點sort.Sort 函式使用的排序演算法是一種由三向切分的快速排序演算法,堆排序演算法和插入排序演算法組成的混合演算法。雖然快速排序是最快的通用排序演算法,但在元素值很少的情況下它比插入順序要慢一些。而堆排序的空間複雜度是常數級別的,且它的時間複雜度在大多數情況下只略遜於其他兩種排序演算法,所以在快速排序中的遞迴達到一定深度的時候,切換至堆排序來節約空間是值得的。這樣的演算法組合使得sort.Sort 函式的時間複雜度在最壞的情況下是 O(N*logN) 的,並且能夠有效地控制對空間的使用,但是不提供穩定性的保證(即在排序過程中不保留陣列或切片值中重複元素的相對位置)。

現在實現 Add 方法,宣告如下:

func (keys *myKeys) Add(k interface{}) bool {
    ok := keys.isAcceptableElem(k)
    if !ok {
        return false
    }
    keys.container = append(keys.container, k)
    // sort.Sort函式會通過對keys的值的Len、Less和Swap方法的呼叫來完成排序。
    // 而在Less方法中,通過compareFunc函式對相鄰的元素值進行比較的。
    sort.Sort(keys)
    return ture
}複製程式碼

6.實現Search方法

現在考慮實現 Remove 方法,但在實現之前需要使用 Search 方法找到指定刪除的元素所處的位置,因此先實現 Search 方法。在 Search 方法中,要搜尋引數 k 代表的值在 container 中對應的索引值。由於 k 的型別是 interface{} 的,所以需要先使用 isAcceptableElem 方法對它進行判定,然後可以通過呼叫 sort.Search 函式來實現搜尋元素值的核心邏輯。sort.Search 函式的宣告如下:

func Search(n int, f func(int) bool) int {
    //省略若干條語句
}複製程式碼

由於 sort.Search 函式使用二分查詢演算法在切片值中搜尋指定的元素值。該搜尋演算法有著穩定的 O(logN) 的時間複雜度,但它要求被搜尋的陣列或切片值必須是有序的,而這裡在新增元素的時候已經保證了container 欄位的值中的元素值是已被排序過的。

從上面的宣告中可知,sort.Search 函式有兩個引數。第一個引數接受的是欲排序的切片值得長度,而第二個引數接受的是一個函式值。這個函式值的含義是:對於一個給定的索引值,判定與之對應的元素值是否等於欲查詢的元素值或者應該排在欲查詢的元素值的右邊。對於引數f的值如下:

func(i int) bool {
    return keys.compareFunc(keys.container[i], k) >= 0
}複製程式碼

如上這個引數 f 應該怎樣理解呢?這裡先假設有這樣一個切片值:

[]int{1, 3, 5, 7, 9, 11, 13, 15}

現在要查詢的元素值是 7,依據二分查詢演算法,sort.Search 函式內部會在第三次折半的時候使用 7 的索引值 3 作為函式f的引數值,函式f的結果值應該是 truesort.Search 函式的執行會結束並返回 7 的索引值 3 作為它的結果值。但是,還有一種情況就是,要查詢的元素根本就不在這個切片值裡,比如 6 或者 8 等等,sort.Search 函式的執行也會在 f(3) 被求值之後結束,且它的結果值會是 4 或者 3,對應的元素都不是要查詢的。

sort.Search 函式的結果值總會在 [0, n] 的範圍內,但結果值並不一定就是欲查詢的元素值所對應的索引值。因此,需要在呼叫 sort.Search 函式的結果值之後再進行一次判斷,如下:

if index < keys.Len() && keys.container[index] == k {
    contains = true
}複製程式碼

其中 index 代表了 sort.Search 函式的結果值,這裡需要先檢查結果值是否在有效的索引範圍之內,然後還需要判斷它所對應的元素值是否就是要查詢的。經過這些分析之後,相信不難實現 *myKeys 型別的 Search 函式。

7.實現Remove方法

實現了 Search 函式就來看看 Remove 函式,通過呼叫 *myKeys 型別的 Search 函式,就可以獲取欲刪除的元素值對應的索引值和它是否被包含在 container 中的判斷結果。如果第二個結果值是 false,就可以直接返回 false,否則就從 container 中刪除掉這個元素值。從切片值中刪除一個元素值有很多種方法,比如使用 for 語句、copy 語句或 append 函式等等。這裡選擇用 append 函式來實現,因為它可以在不增加時間複雜度和空間複雜度的情況下使用更少的程式碼來完成功能,且不降低可讀性。如下實現了刪除一個元素值的功能:

keys.container = append(keys.container[0: index],keys.container[index+1: ]…)複製程式碼

如上程式碼充分地使用了切片表示式和 append 函式,使用如下切片表示式:

keys.container[0: index]//取出container欄位的值中的在欲刪除元素值之前的子元素序列
keys.container[index+1: ]// 取出container欄位的值中的在欲刪除元素值之後的子元素序列複製程式碼

接著通過 append 函式將兩個元素子序列拼接起來,可以在第二個引數值之後新增“…”以表示把第二個引數值中的每個元素值都作為傳給 append 函式的獨立引數,這樣就把第二個子序列中的所有元素值逐個追加到了第一個子序列的尾部,最後把拼接後的元素序列賦值給了 container 欄位。

8.實現Clear方法

func (keys *myKeys) clear() {
    keys.container = make([]interface{}, 0)
}複製程式碼

9.實現Get方法

func (keys *myKeys) Get(index int) interface{} {
    if index >= keys.Len() {
        return nil
    }
    return keys.container[index]
}
複製程式碼

10.實現GetAll方法

func (keys *myKeys) GetAll() []interface{} {
    initialLen := len(keys.container)
    snapshot := make([]interface{}, initialLen)
    actualLen := 0
    for _, key := range keys.container {
        if actualLen < initialLen {
            snapshot[actualLen] = key
        } else {
            snapshot = append(snapshot, key)
        }
        actualLen++
    }
    if actualLen < initialLen {
        snapshot = snapshot[:actualLen]
    }
    return snapshot
}複製程式碼

11.實現ElemType和CompareFunc方法

func (keys *myKeys) ElemType() reflect.Type {
    return keys.elemType
}

func (keys *myKeys) CompareFunc() CompareFunction {
    return keys.compareFunc
}複製程式碼

12.實現String方法

//String方法被用於生成可讀性更好的接收者值的字串表示形式。
func (keys *myKeys) String() string {
    var buf bytes.Buffer
    buf.WriteString("Keys<")
    buf.WriteString(keys.elemType.Kind().String())
    buf.WriteString(">{")
    first := true
    buf.WriteString("[")
    for _, key := range keys.container {
        if first {
            first = false
        } else {
            buf.WriteString(" ")
        }
        buf.WriteString(fmt.Sprintf("%v", key))
    }
    buf.WriteString("]")
    buf.WriteString("}")
    return buf.String()
}複製程式碼

13.定義NewKeys函式

目前已經完成了 myKeys 型別以及相關方法的編寫,還應該編寫一個用於初始化 *myKeys 型別值的函式。這個函式名為 NewKeys,結果型別為 Keys,宣告如下:

func NewKeys ( compareFunc func(interface{}, interface{}) int8, elemType reflect.Type) Keys {
    //因為只有*myKeys型別的方法集合中才包含了Keys介面型別中宣告的所有方法,
    //所以下面返回的是一個*myKeys型別值,而不是一個myKeys型別值。
    return &myKeys {
        container :         make([]interface{}, 0),
        compareFunc :   compareFunc,
        elemType :      elemType,
    }
}複製程式碼

從上可以看出,在 NewKeys 函式的引數宣告列表中沒有與 container 欄位相對應的引數宣告,原因是 container 欄位的值總應該是一個長度為 0[]interface{} 型別值,它不必由 NewKeys 函式的呼叫方提供。另外,NewKeys 函式的compareFunc 引數和 elemType 引數之間的關係,也要滿足之前上文提到的約束條件。

14.重新理解OrderedMap

至此我們已經編寫完成了 OrderedMap 型別所需要用到的最核心的資料型別 KeysmyKeys
由於有了 Keys 介面型別,OrderedMap 型別的宣告被修改為:

type myOrderedMap struct {
    keys        Keys
    elemType    reflect.Type
    m           map[interface{}]interface{}
}複製程式碼

如上更改了該型別的名稱,這裡要宣告一個介面型別來描述有序字典型別所提供的功能,而 OrderedMap 更適合作為這個介面型別的名稱。宣告如下:

// 泛化的Map的介面型別
type OrderedMap interface {
    // 獲取給定鍵值對應的元素值。若沒有對應元素值則返回nil。
    Get(key interface{}) interface{}
    // 新增鍵值對,並返回與給定鍵值對應的舊的元素值。若沒有舊元素值則返回(nil, true)。
    Put(key interface{}, elem interface{}) (interface{}, bool)
    // 刪除與給定鍵值對應的鍵值對,並返回舊的元素值。若沒有舊元素值則返回nil。
    Remove(key interface{}) interface{}
    // 清除所有的鍵值對。
    Clear()
    // 獲取鍵值對的數量。
    Len() int
    // 判斷是否包含給定的鍵值。
    Contains(key interface{}) bool
    // 獲取已排序的鍵值所組成的切片值。
    Keys() []interface{}
    // 獲取已排序的元素值所組成的切片值。
    Elems() []interface{}
    // 獲取已包含的鍵值對所組成的字典值。
    ToMap() map[interface{}]interface{}
    // 獲取鍵的型別。
    KeyType() reflect.Type
    // 獲取元素的型別。
    ElemType() reflect.Type
}複製程式碼

這裡要使 *myOrderedMap 型別成為 OrderedMap 介面型別的實現型別。雖然方法不多,但實現起來並不難,完成後還可以編寫一個 NewOrderedMap 函式,用於將初始化好的 *myOrderedMap 型別值作為結果值返回。

完整的 myOrderedMap 型別以及相關方法的實現,如下連結:

github.com/hyper-carro…

本文來自:CSDN部落格

感謝作者:u012855229

檢視原文:Go語言實戰_自定義OrderedMap


相關文章