获取用户列表为空_数据结构和算法(Golang实现)(15)常见数据结构-列表
列表
一、列表 List
我們又經常聽到 列表 List 數據結構,其實這只是更宏觀的統稱,表示存放數據的隊列。
列表 List:存放數據,數據按順序排列,可以依次入隊和出隊,有序號關系,可以取出某序號的數據。先進先出的 隊列 (queue) 和先進后出的 棧(stack) 都是列表。大家也經常聽說一種叫 線性表 的數據結構,表示具有相同特性的數據元素的有限序列,實際上就是 列表 的同義詞。我們一般寫算法進行數據計算,數據處理,都需要有個地方來存數據,我們可以使用封裝好的數據結構 List:
列表的實現有 順序表示 或 鏈式表示。
順序表示:指的是用一組 地址連續的存儲單元 依次存儲線性表的數據元素,稱為線性表的 順序存儲結構。它以 物理位置相鄰 來表示線性表中數據元素間的邏輯關系,可隨機存取表中任一元素。順序表示的又叫 順序表,也就是用數組來實現的列表。
鏈式表示:指的是用一組 任意的存儲單元 存儲線性表中的數據元素,稱為線性表的 鏈式存儲結構。它的存儲單元可以是連續的,也可以是不連續的。在表示數據元素之間的邏輯關系時,除了存儲其本身的信息之外,還需存儲一個指示其直接后繼的信息,也就是用鏈表來實現的列表。
我們在前面已經實現過這兩種表示的數據結構:先進先出的 隊列 (queue) 和先進后出的 棧(stack)。接下來我們會來實現鏈表形式的雙端列表,也叫雙端隊列,這個數據結構應用場景更廣泛一點。在實際工程應用上,緩存數據庫 Redis 的 列表List 基本類型就是用它來實現的。
二、實現雙端列表
雙端列表,也可以叫雙端隊列
我們會用雙向鏈表來實現這個數據結構:
// 雙端列表,雙端隊列 type DoubleList struct {head *ListNode // 指向鏈表頭部tail *ListNode // 指向鏈表尾部len int // 列表長度lock sync.Mutex // 為了進行并發安全pop操作 }// 列表節點 type ListNode struct {pre *ListNode // 前驅節點next *ListNode // 后驅節點value string // 值 }設計結構體 DoubleList 指向隊列頭部 head 和尾部 tail 的指針字段,方便找到鏈表最前和最后的節點,并且鏈表節點之間是雙向鏈接的,鏈表的第一個元素的前驅節點為 nil,最后一個元素的后驅節點也為 nil。如圖:
我們實現的雙端列表和 Golang 標準庫 container/list 中實現的不一樣,感興趣的可以閱讀標準庫的實現。
2.1.列表節點普通操作
// 獲取節點值 func (node *ListNode) GetValue() string {return node.value }// 獲取節點前驅節點 func (node *ListNode) GetPre() *ListNode {return node.pre }// 獲取節點后驅節點 func (node *ListNode) GetNext() *ListNode {return node.next }// 是否存在后驅節點 func (node *ListNode) HashNext() bool {return node.pre != nil }// 是否存在前驅節點 func (node *ListNode) HashPre() bool {return node.next != nil }// 是否為空節點 func (node *ListNode) IsNil() bool {return node == nil }以上是對節點結構體 ListNode 的操作,主要判斷節點是否為空,有沒有后驅和前驅節點,返回值等,時間復雜度都是 O(1)。
2.2.從頭部開始某個位置前插入新節點
// 添加節點到鏈表頭部的第N個元素之前,N=0表示新節點成為新的頭部 func (list *DoubleList) AddNodeFromHead(n int, v string) {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過列表長度,一定找不到,panicif n > list.len {panic("index out")}// 先找出頭部node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}// 新節點newNode := new(ListNode)newNode.value = v// 如果定位到的節點為空,表示列表為空,將新節點設置為新頭部和新尾部if node.IsNil() {list.head = newNodelist.tail = newNode} else {// 定位到的節點,它的前驅pre := node.pre// 如果定位到的節點前驅為nil,那么定位到的節點為鏈表頭部,需要換頭部if pre.IsNil() {// 將新節點鏈接在老頭部之前newNode.next = nodenode.pre = newNode// 新節點成為頭部list.head = newNode} else {// 將新節點插入到定位到的節點之前// 定位到的節點的前驅節點 pre 現在鏈接到新節點上pre.next = newNodenewNode.pre = pre// 定位到的節點的后驅節點 node.next 現在鏈接到新節點上node.next.pre = newNodenewNode.next = node.next}}// 列表長度+1list.len = list.len + 1 }首先加鎖實現并發安全。然后判斷索引是否超出列表長度:
// 索引超過列表長度,一定找不到,panicif n > list.len {panic("index out")}如果 n=0 表示新節點想成為新的鏈表頭部,n=1 表示插入到鏈表頭部數起第二個節點之前,新節點成為第二個節點,以此類推。
首先,找出頭部:node := list.head,然后往后面遍歷,定位到索引指定的節點 node:
// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}接著初始化新節點:newNode := new(ListNode)。
定位到的節點有三種情況,我們需要在該節點之前插入新節點:
判斷定位到的節點 node 是否為空,如果為空,表明列表沒有元素,將新節點設置為新頭部和新尾部。
否則找到定位到的節點的前驅節點:pre := node.pre。
如果前驅節點為空:pre.IsNil(),表明定位到的節點 node 為頭部,那么新節點要取代它,成為新的頭部:
if pre.IsNil() {// 將新節點鏈接在老頭部之前newNode.next = nodenode.pre = newNode// 新節點成為頭部list.head = newNode}新節點成為新的頭部,需要將新節點的后驅設置為老頭部:newNode.next = node,老頭部的前驅為新頭部:node.pre = newNode,并且新頭部變化:list.head = newNode。
如果定位到的節點的前驅節點不為空,表明定位到的節點 node 不是頭部節點,那么我們只需將新節點鏈接到節點 node 之前即可:
// 定位到的節點的前驅節點 pre 現在鏈接到新節點前pre.next = newNodenewNode.pre = pre// 定位到的節點鏈接到新節點之后newNode.next = nodenode.pre = newNode先將定位到的節點的前驅節點和新節點綁定,因為現在新節點插在前面了,把定位節點的前驅節點的后驅設置為新節點:pre.next = newNode,新節點的前驅設置為定位節點的前驅節點:newNode.pre = pre。
同時,定位到的節點現在要鏈接到新節點之后,所以新節點的后驅設置為:newNode.next = node,定位到的節點的前驅設置為:node.pre = newNode。
最后,鏈表長度加一。
大部分時間花在遍歷位置上,如果 n=0,那么時間復雜度為 O(1),否則為 O(n)。
2.3.從尾部開始某個位置后插入新節點
// 添加節點到鏈表尾部的第N個元素之后,N=0表示新節點成為新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過列表長度,一定找不到,panicif n > list.len {panic("index out")}// 先找出尾部node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}// 新節點newNode := new(ListNode)newNode.value = v// 如果定位到的節點為空,表示列表為空,將新節點設置為新頭部和新尾部if node.IsNil() {list.head = newNodelist.tail = newNode} else {// 定位到的節點,它的后驅next := node.next// 如果定位到的節點后驅為nil,那么定位到的節點為鏈表尾部,需要換尾部if next.IsNil() {// 將新節點鏈接在老尾部之后node.next = newNodenewNode.pre = node// 新節點成為尾部list.tail = newNode} else {// 將新節點插入到定位到的節點之后// 新節點鏈接到定位到的節點之后newNode.pre = nodenode.next = newNode// 定位到的節點的后驅節點鏈接在新節點之后newNode.next = nextnext.pre = newNode}}// 列表長度+1list.len = list.len + 1 }操作和頭部插入節點相似,自行分析。
2.4.從頭部開始某個位置獲取列表節點
// 從頭部開始往后找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromHead(n int) *ListNode {// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取頭部節點node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}return node }如果索引超出或等于列表長度,那么找不到節點,返回空。
否則從頭部開始遍歷,拿到節點。
時間復雜度為:O(n)。
2.5.從尾部開始某個位置獲取列表節點
// 從尾部開始往前找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromTail(n int) *ListNode {// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取尾部節點node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}return node }操作和從頭部獲取節點一樣,請自行分析。
2.6.從頭部開始移除并返回某個位置的節點
// 從頭部開始往后找,獲取第N+1個位置的節點,并移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取頭部node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}// 移除的節點的前驅和后驅pre := node.prenext := node.next// 如果前驅和后驅都為nil,那么移除的節點為鏈表唯一節點if pre.IsNil() && next.IsNil() {list.head = nillist.tail = nil} else if pre.IsNil() {// 表示移除的是頭部節點,那么下一個節點成為頭節點list.head = nextnext.pre = nil} else if next.IsNil() {// 表示移除的是尾部節點,那么上一個節點成為尾節點list.tail = prepre.next = nil} else {// 移除的是中間節點pre.next = nextnext.pre = pre}// 節點減一list.len = list.len - 1return node }首先加并發鎖實現并發安全。先判斷索引是否超出列表長度:n >= list.len,如果超出直接返回空指針。
獲取頭部,然后遍歷定位到第 N+1 個位置的元素:node = node.next。
定位到的并要移除的節點有三種情況發生:
查看要移除的節點的前驅和后驅:
// 移除的節點的前驅和后驅pre := node.prenext := node.next如果前驅和后驅都為空:pre.IsNil() && next.IsNil(),那么要移除的節點是鏈表中唯一的節點,直接將列表頭部和尾部置空即可。
如果前驅節點為空:pre.IsNil(),表示移除的是頭部節點,那么頭部節點的下一個節點要成為新的頭部:list.head = next,并且這時新的頭部前驅要設置為空:next.pre = nil。
同理,如果后驅節點為空:next.IsNil(),表示移除的是尾部節點,需要將尾部節點的前一個節點設置為新的尾部:list.tail = pre,并且這時新的尾部后驅要設置為空:pre.next = nil。
如果移除的節點處于兩個節點之間,那么將這兩個節點鏈接起來即可:
// 移除的是中間節點pre.next = nextnext.pre = pre最后,列表長度減一。
主要的耗時用在定位節點上,其他的操作都是鏈表鏈接,可以知道時間復雜度為:O(n)。
2.7.從尾部開始移除并返回某個位置的節點
// 從尾部開始往前找,獲取第N+1個位置的節點,并移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取尾部node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}// 移除的節點的前驅和后驅pre := node.prenext := node.next// 如果前驅和后驅都為nil,那么移除的節點為鏈表唯一節點if pre.IsNil() && next.IsNil() {list.head = nillist.tail = nil} else if pre.IsNil() {// 表示移除的是頭部節點,那么下一個節點成為頭節點list.head = nextnext.pre = nil} else if next.IsNil() {// 表示移除的是尾部節點,那么上一個節點成為尾節點list.tail = prepre.next = nil} else {// 移除的是中間節點pre.next = nextnext.pre = pre}// 節點減一list.len = list.len - 1return node }操作和從頭部移除節點相似,請自行分析。
2.8.完整例子
package mainimport ("fmt""sync" )// 雙端列表,雙端隊列 type DoubleList struct {head *ListNode // 指向鏈表頭部tail *ListNode // 指向鏈表尾部len int // 列表長度lock sync.Mutex // 為了進行并發安全pop操作 }// 列表節點 type ListNode struct {pre *ListNode // 前驅節點next *ListNode // 后驅節點value string // 值 }// 獲取節點值 func (node *ListNode) GetValue() string {return node.value }// 獲取節點前驅節點 func (node *ListNode) GetPre() *ListNode {return node.pre }// 獲取節點后驅節點 func (node *ListNode) GetNext() *ListNode {return node.next }// 是否存在后驅節點 func (node *ListNode) HashNext() bool {return node.pre != nil }// 是否存在前驅節點 func (node *ListNode) HashPre() bool {return node.next != nil }// 是否為空節點 func (node *ListNode) IsNil() bool {return node == nil }// 返回列表長度 func (list *DoubleList) Len() int {return list.len }// 添加節點到鏈表頭部的第N個元素之前,N=0表示新節點成為新的頭部 func (list *DoubleList) AddNodeFromHead(n int, v string) {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過列表長度,一定找不到,panicif n > list.len {panic("index out")}// 先找出頭部node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}// 新節點newNode := new(ListNode)newNode.value = v// 如果定位到的節點為空,表示列表為空,將新節點設置為新頭部和新尾部if node.IsNil() {list.head = newNodelist.tail = newNode} else {// 定位到的節點,它的前驅pre := node.pre// 如果定位到的節點前驅為nil,那么定位到的節點為鏈表頭部,需要換頭部if pre.IsNil() {// 將新節點鏈接在老頭部之前newNode.next = nodenode.pre = newNode// 新節點成為頭部list.head = newNode} else {// 將新節點插入到定位到的節點之前// 定位到的節點的前驅節點 pre 現在鏈接到新節點前pre.next = newNodenewNode.pre = pre// 定位到的節點鏈接到新節點之后newNode.next = nodenode.pre = newNode}}// 列表長度+1list.len = list.len + 1 }// 添加節點到鏈表尾部的第N個元素之后,N=0表示新節點成為新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過列表長度,一定找不到,panicif n > list.len {panic("index out")}// 先找出尾部node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}// 新節點newNode := new(ListNode)newNode.value = v// 如果定位到的節點為空,表示列表為空,將新節點設置為新頭部和新尾部if node.IsNil() {list.head = newNodelist.tail = newNode} else {// 定位到的節點,它的后驅next := node.next// 如果定位到的節點后驅為nil,那么定位到的節點為鏈表尾部,需要換尾部if next.IsNil() {// 將新節點鏈接在老尾部之后node.next = newNodenewNode.pre = node// 新節點成為尾部list.tail = newNode} else {// 將新節點插入到定位到的節點之后// 新節點鏈接到定位到的節點之后newNode.pre = nodenode.next = newNode// 定位到的節點的后驅節點鏈接在新節點之后newNode.next = nextnext.pre = newNode}}// 列表長度+1list.len = list.len + 1 }// 返回列表鏈表頭結點 func (list *DoubleList) First() *ListNode {return list.head }// 返回列表鏈表尾結點 func (list *DoubleList) Last() *ListNode {return list.tail }// 從頭部開始往后找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromHead(n int) *ListNode {// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取頭部節點node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}return node }// 從尾部開始往前找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromTail(n int) *ListNode {// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取尾部節點node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}return node }// 從頭部開始往后找,獲取第N+1個位置的節點,并移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取頭部node := list.head// 往后遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.next}// 移除的節點的前驅和后驅pre := node.prenext := node.next// 如果前驅和后驅都為nil,那么移除的節點為鏈表唯一節點if pre.IsNil() && next.IsNil() {list.head = nillist.tail = nil} else if pre.IsNil() {// 表示移除的是頭部節點,那么下一個節點成為頭節點list.head = nextnext.pre = nil} else if next.IsNil() {// 表示移除的是尾部節點,那么上一個節點成為尾節點list.tail = prepre.next = nil} else {// 移除的是中間節點pre.next = nextnext.pre = pre}// 節點減一list.len = list.len - 1return node }// 從尾部開始往前找,獲取第N+1個位置的節點,并移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode {// 加并發鎖list.lock.Lock()defer list.lock.Unlock()// 索引超過或等于列表長度,一定找不到,返回空指針if n >= list.len {return nil}// 獲取尾部node := list.tail// 往前遍歷拿到第 N+1 個位置的元素for i := 1; i <= n; i++ {node = node.pre}// 移除的節點的前驅和后驅pre := node.prenext := node.next// 如果前驅和后驅都為nil,那么移除的節點為鏈表唯一節點if pre.IsNil() && next.IsNil() {list.head = nillist.tail = nil} else if pre.IsNil() {// 表示移除的是頭部節點,那么下一個節點成為頭節點list.head = nextnext.pre = nil} else if next.IsNil() {// 表示移除的是尾部節點,那么上一個節點成為尾節點list.tail = prepre.next = nil} else {// 移除的是中間節點pre.next = nextnext.pre = pre}// 節點減一list.len = list.len - 1return node }func main() {list := new(DoubleList)// 在列表頭部插入新元素list.AddNodeFromHead(0, "I")list.AddNodeFromHead(0, "love")list.AddNodeFromHead(0, "you")// 在列表尾部插入新元素list.AddNodeFromTail(0, "may")list.AddNodeFromTail(0, "happy")// 正常遍歷,比較慢for i := 0; i < list.Len(); i++ {// 從頭部開始索引node := list.IndexFromHead(i)// 節點為空不可能,因為list.Len()使得索引不會越界if !node.IsNil() {fmt.Println(node.GetValue())}}fmt.Println("----------")// 正常遍歷,特別快// 先取出第一個元素first := list.First()for !first.IsNil() {// 如果非空就一直遍歷fmt.Println(first.GetValue())// 接著下一個節點first = first.GetNext()}fmt.Println("----------")// 元素一個個 POP 出來for {node := list.PopFromHead(0)if node.IsNil() {// 沒有元素了,直接返回break}fmt.Println(node.GetValue())}fmt.Println("----------")fmt.Println("len", list.Len()) }輸出:
you love I may happy ---------- you love I may happy ---------- you love I may happy ---------- len 0首先,先從列表頭部插入三個新元素,然后從尾部插入兩個新元素,然后用三種方式進行遍歷,兩種只是查看元素,一種是遍歷移除元素。
系列文章入口
我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發于
目錄 · 數據結構和算法(Golang實現)?goa.lenggirl.com- 數據結構和算法(Golang實現)(1)簡單入門Golang-前言
- 數據結構和算法(Golang實現)(2)簡單入門Golang-包、變量和函數
- 數據結構和算法(Golang實現)(3)簡單入門Golang-流程控制語句
- 數據結構和算法(Golang實現)(4)簡單入門Golang-結構體和方法
- 數據結構和算法(Golang實現)(5)簡單入門Golang-接口
- 數據結構和算法(Golang實現)(6)簡單入門Golang-并發、協程和信道
- 數據結構和算法(Golang實現)(7)簡單入門Golang-標準庫
- 數據結構和算法(Golang實現)(8.1)基礎知識-前言
- 數據結構和算法(Golang實現)(8.2)基礎知識-分治法和遞歸
- 數據結構和算法(Golang實現)(9)基礎知識-算法復雜度及漸進符號
- 數據結構和算法(Golang實現)(10)基礎知識-算法復雜度主方法
- 數據結構和算法(Golang實現)(11)常見數據結構-前言
- 數據結構和算法(Golang實現)(12)常見數據結構-鏈表
- 數據結構和算法(Golang實現)(13)常見數據結構-可變長數組
- 數據結構和算法(Golang實現)(14)常見數據結構-棧和隊列
- 數據結構和算法(Golang實現)(15)常見數據結構-列表
- 數據結構和算法(Golang實現)(16)常見數據結構-字典
- 數據結構和算法(Golang實現)(17)常見數據結構-樹
- 數據結構和算法(Golang實現)(18)排序算法-前言
- 數據結構和算法(Golang實現)(19)排序算法-冒泡排序
- 數據結構和算法(Golang實現)(20)排序算法-選擇排序
- 數據結構和算法(Golang實現)(21)排序算法-插入排序
- 數據結構和算法(Golang實現)(22)排序算法-希爾排序
- 數據結構和算法(Golang實現)(23)排序算法-歸并排序
- 數據結構和算法(Golang實現)(24)排序算法-優先隊列及堆排序
- 數據結構和算法(Golang實現)(25)排序算法-快速排序
- 數據結構和算法(Golang實現)(26)查找算法-哈希表
- 數據結構和算法(Golang實現)(27)查找算法-二叉查找樹
- 數據結構和算法(Golang實現)(28)查找算法-AVL樹
- 數據結構和算法(Golang實現)(29)查找算法-2-3樹和左傾紅黑樹
- 數據結構和算法(Golang實現)(30)查找算法-2-3-4樹和普通紅黑樹
總結
以上是生活随笔為你收集整理的获取用户列表为空_数据结构和算法(Golang实现)(15)常见数据结构-列表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 挑战握手协议_什么是挑战握手
- 下一篇: java过滤器经典案例_JAVA语言基础