首頁 > 軟體

GoLang完整實現快速列表

2022-12-19 14:01:39

快速列表介紹

快速列表(quicklist)是Redis中特有的一種資料結構,主要是為了解決雙端連結串列的弊端:雙端連結串列的附加空間比較高,因為prevnext指標會佔掉一部分的空間(64位元系統佔用8 + 8 = 16位元組).而且連結串列的每個節點都是單獨分配記憶體,會加劇記憶體的碎片化。

Redis中的快速列表實際上是zipList(經過優化過的陣列)和linkedList的混合體,它把zipList放在linkedList的每個結點中,實現緊湊儲存。如下圖所示:

實現快速列表

快速列表的結構

由於Go語言自帶slice這種操作方便的“動態陣列”結構,所以我給連結串列的每個節點中都分配一個容量為1024大小的切片,那麼一個連結串列節點就可以看作是一頁,頁大小就是1024。

頁大小

首先定義頁大小為1024:

// pageSize 一頁的大小
const pageSize = 1024

表頭結構

// QuickList 快速列表是在頁(切片)之上建立的連結串列
// QuickList 比普通連結串列的插入、遍歷以及記憶體有著更好的效能
type QuickList struct {
	data *list.List // 每一頁就是interface{}的切片,大小為1024
	size int
}

表頭結構中的data欄位直接使用了Go語言list包中的List結構:

// 雙向連結串列檔頭結構
type List struct {
	root Element // 哨兵節點
	len  int     // 連結串列的節點個數
}
// 雙向連結串列的節點
type Element struct {
	// 前向和後向指標
	next, prev *Element
	// 表頭結構
	list *List
	// 值域
	Value interface{}
}

Go語言自帶的list包實現了對雙向連結串列的封裝,雙向連結串列的節點元素是interface{}型別,利用這種方式實現了泛型。

我們對快速列表的實現,使得上述雙向連結串列節點中儲存的實際上是容量為1024的切片,此後對於連結串列的相關操作,直接呼叫list包向外暴露的API即可。

快速列表的迭代器

快速列表的迭代器中有三個欄位:連結串列的節點node(可以看成一頁),元素在頁中的偏移量、表頭結構。

這樣實現的迭代器,使得迭代器既可以在元素之前迭代,也可以在頁之間快速迭代。

// iterator 快速列表的迭代器,在[-1, ql.Len()]之間迭代
type iterator struct {
	// 快速列表的一頁
	node *list.Element
	// 元素下標在頁中的偏移量
	offset int
	ql     *QuickList
}

使用迭代器返回一個元素

使用迭代器返回一個元素的複雜度為O(1),一個元素的位置可以通過 頁的位置 + 該元素在頁中的位置 快速定位。

// 使用迭代器返回一個元素
func (iter *iterator) get() interface{} {
	return iter.page()[iter.offset]
}

返回迭代器對應的那一頁

上面我們說過,連結串列的節點元素其實就是一個容量為1024的slice,通過型別斷言直接返回即可。

// 返回迭代器對應的那一頁
func (iter *iterator) page() []interface{} {
	return iter.node.Value.([]interface{})
}

根據元素下標返回對應的迭代器

快速列表查詢元素效率比雙向列表要快,首先利用迭代器一頁一頁進行迭代,當確定了元素在哪一頁後,利用元素的下標直接在頁內的slice中直接定位即可。

func (ql *QuickList) find(index int) *iterator {
	if ql == nil {
		panic("list is nil")
	}
	if index < 0 || index >= ql.size {
		panic("index out of bound")
	}
	var n *list.Element
	// 儲存當前頁的所有元素
	var page []interface{}
	// 累加遍歷到當前頁為止,前面的所有元素數量
	var pageBeg int
	if index < ql.size/2 {
		// 從表頭進行查詢
		n = ql.data.Front()
		pageBeg = 0
		for {
			page = n.Value.([]interface{})
			if pageBeg+len(page) > index {
				break
			}
			pageBeg += len(page)
			n = n.Next()
		}
	} else {
		// 從表尾進行查詢
		n = ql.data.Back()
		pageBeg = ql.size
		for {
			page = n.Value.([]interface{})
			pageBeg -= len(page)
			if pageBeg <= index {
				break
			}
			n = n.Prev()
		}
	}
	pageOffset := index - pageBeg
	return &iterator{
		node:   n,
		offset: pageOffset,
		ql:     ql,
	}
}

向後迭代一位

// next 頁內迭代器,向後迭代一位
// 如果當前元素下標未出界且不在最後一位,就向後移動一位,返回true
// 如果當前元素下標在快速列表的最後一頁且是最後一個元素,直接返回false
// 如果當前元素下標不在快速列表的最後一頁,但是是當前頁的最後一個元素,跳轉到下一頁,返回true
func (iter *iterator) next() bool {
	// 得到迭代器對應的那一頁
	page := iter.page()
	// 當前位置未出界且不在最後一位,就向後移動一位,返回true
	if iter.offset < len(page)-1 {
		iter.offset++
		return true
	}
	// 當前元素在快速列表的最後一頁且是最後一個元素,直接返回false
	if iter.node == iter.ql.data.Back() {
		// already at last node
		iter.offset = len(page)
		return false
	}
	// 當前元素不在快速列表的最後一頁,但是是當前頁的最後一個元素,跳轉到下一頁,返回true
	iter.offset = 0
	iter.node = iter.node.Next()
	return true
}

往前迭代一位

// prev 頁內迭代器,向前迭代一位
func (iter *iterator) prev() bool {
	if iter.offset > 0 {
		iter.offset--
		return true
	}
	if iter.node == iter.ql.data.Front() {
		iter.offset = -1
		return false
	}
	iter.node = iter.node.Prev()
	prevPage := iter.node.Value.([]interface{})
	iter.offset = len(prevPage) - 1
	return true
}

新增和插入元素

向表尾新增一個元素

向表尾新增元素需要考慮三種情況:

  • 列表是空的,建立新的一頁,新增到表尾即可。
  • 表尾節點那一頁是滿的,獲取表尾節點,建立新的一頁,新增到表尾節點的後面即可。
  • 表尾節點那一頁不是滿的,正常新增到表尾即可。
// Add 新增元素到表尾
func (ql *QuickList) Add(val interface{}) {
	ql.size++
	// 列表是空的
	if ql.data.Len() == 0 {
		page := make([]interface{}, 0, pageSize)
		page = append(page, val)
		ql.data.PushBack(page)
		return
	}
	// 獲取表尾節點
	backNode := ql.data.Back()
	backPage := backNode.Value.([]interface{})
	// 表尾節點頁滿了,需要新建立一頁
	if len(backPage) == cap(backPage) {
		page := make([]interface{}, 0, pageSize)
		page = append(page, val)
		ql.data.PushBack(page)
		return
	}
	// 預設將節點新增進表尾頁中
	backPage = append(backPage, val)
	backNode.Value = backPage
}

根據下標插入一個元素

// Insert 插入元素
// 插入元素的策略分三種情況:
// 1. 向最後一頁的最後一個位置插入元素,直接掉用ql.Add()插入即可
// 2. 某一頁插入一個元素,且該頁未滿,直接插入該頁即可
// 3. 某一頁插入一個元素,該頁滿了,就新建立一頁,然後將前512個元素留在原來那頁,將後512個元素移到新的頁中,
//    新插入的元素,如果下標在[0,512]之間,就插入到原來頁,如果下標在[516, 1024]之間,就插入到新建立的頁中
func (ql *QuickList) Insert(index int, val interface{}) {
	// 向表尾插入元素
	if index == ql.size {
		ql.Add(val)
		return
	}
	iter := ql.find(index)
	page := iter.node.Value.([]interface{})
	// 如果待插入頁的元素小於1024,直接插入到該頁即可
	if len(page) < pageSize {
		// insert into not full page
		page = append(page[:iter.offset+1], page[iter.offset:]...)
		page[iter.offset] = val
		iter.node.Value = page
		ql.size++
		return
	}
	// 待插入頁的元素已經滿1024,就需要新建立一頁
	var nextPage []interface{}
	// 後一半的元素放在新建立的頁中,前一半元素放在原來的頁中
	nextPage = append(nextPage, page[pageSize/2:]...) // pageSize must be even
	page = page[:pageSize/2]
	// 待插入元素的下標小於512,插到前面那頁
	if iter.offset < len(page) {
		page = append(page[:iter.offset+1], page[iter.offset:]...)
		page[iter.offset] = val
	} else {
		// 待插入元素的下標大於512,插到後面那頁
		i := iter.offset - pageSize/2
		nextPage = append(nextPage[:i+1], nextPage[i:]...)
		nextPage[i] = val
	}
	// 儲存當前頁和新建立的下一頁
	iter.node.Value = page
	ql.data.InsertAfter(nextPage, iter.node)
	ql.size++
}

刪除元素

// 刪除元素
// 刪除元素分為三種情況:
// 1.刪除後的頁不為空,且刪除的不是該頁的最後一個元素,什麼都不用管
// 2.刪除後的頁不為空,且刪除的是該頁的最後一個元素,需要將迭代器移動到下一頁的最後一個元素
// 3.刪除的頁為空(需要刪除該頁),且刪除的頁是最後一頁,將迭代器置空
// 4.刪除的頁為空(需要刪除該頁),且刪除的頁不是最後一頁,將迭代器指向下一頁
func (iter *iterator) remove() interface{} {
	page := iter.page()
	val := page[iter.offset]
	// 先直接在頁中刪除這個元素
	page = append(page[:iter.offset], page[iter.offset+1:]...)
	// 如果刪除後的頁不為空,只更新iter.offset即可
	if len(page) > 0 {
		iter.node.Value = page
		// 如果刪除的是頁中的最後一個元素,那麼迭代器需要移動到下一頁的第一個元素
		if iter.offset == len(page) {
			if iter.node != iter.ql.data.Back() {
				iter.node = iter.node.Next()
				iter.offset = 0
			}
		}
	} else {
		// 如果刪除後的頁為空,需要刪除該頁
		// 如果刪除的是最後一頁,迭代器需要置空
		if iter.node == iter.ql.data.Back() {
			iter.ql.data.Remove(iter.node)
			iter.node = nil
			iter.offset = 0
		} else {
			// 如果刪除的不是最後一頁,迭代器需要指向下一頁
			nextNode := iter.node.Next()
			iter.ql.data.Remove(iter.node)
			iter.node = nextNode
			iter.offset = 0
		}
	}
	iter.ql.size--
	return val
}

遍歷快速列表

// Consumer 用於遍歷中斷的函數,返回true表示繼續遍歷,可以在Consumer中呼叫自定義函數
type Consumer func(i int, v interface{}) bool
// ForEach 遍歷快速列表中的元素
// 如果consumer返回false,結束遍歷
func (ql *QuickList) ForEach(consumer Consumer) {
	if ql == nil {
		panic("list is nil")
	}
	if ql.Len() == 0 {
		return
	}
	iter := ql.find(0)
	i := 0
	for {
		goNext := consumer(i, iter.get())
		if !goNext {
			break
		}
		i++
		// 遍歷到表尾,結束
		if !iter.next() {
			break
		}
	}
}

完整實現

https://github.com/omlight95/GoRedis/blob/master/datastruct/list/quicklist.go

到此這篇關於GoLang完整實現快速列表的文章就介紹到這了,更多相關Go快速列表內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com