Go语言映射与链表

Go语言映射与链表入门

映射

映射(map)是一种集合,它的每个元素都带有 key。这个 key 用于标识元素,在同一个 map 对象中,元素的值可以重复出现,但 key 必须是唯一的。

数组、切片类型的对象是通过 int 类型的索引来访问元素的,而映射类型的对象是通过 key 来访问元素的,key 可以是任意类型。

映射对象的初始化

映射类型的表示格式如下:

map[keyType]valueType

例如,元素类型为 string,key 类型为 int 的映射类型可以表示为:

map[int]string

仅仅声明映射类型的变量是不能直接操作的,下面代码会发生错误。

var m map[byte]string
m[12] = "abc"
m[24] = "opq"
m[48] = "efg"

这是由于映射类型的默认值是 nil,必须初始化之后才能使用。调用 make 函数可以创建映射对象实例。例如:

var m1 = make(map[uint16]string)

m1 的元素类型为 string,对应的 key 类型为 uint。分配实例后,就可以向映射对象添加元素了。

m1[20] = "fly"
m1[60] = "play"

另一种方法是直接使用 map 表达式来实例化。

var m2 = map[rune]float64{}

注意最后面的一对大括号不能省略。m2 的元素类型为 float64,其 key 类型为 rune。和 make 函数的调用结果类似,创建映射实例后,就可以添加元素了。

m2['a'] = 1.0000752
m2['b'] = -0.00016

当然,也可以在实例化的时候初始化元素列表。

var m3 = map[string]uint64{
    "item 1": 8150,
    "item 2": 17990,
    "item 3": 28005,
    "item 4": 540,
}

实例化之后,可以继续添加元素。

m3["item 5"] = 1294
m3["item 6"] = 290
m3["item 7"] = 61625

访问映射对象的元素

映射对象的元素可以通过 key 来读写。例如:

x[key] = value          // 设置元素
value = x[key]         // 获取元素

下面代码实例化了一个映射对象,然后向其中三个元素赋值。

var mt = make(map[int32]byte)
mt[1] = 25
mt[2] = 26
mt[3] = 27

此映射对象的元素类型为 byte(uint8 类型的别名,表示元素为一字节),元素的 key 为 int32 类型。

通过 key 可以访问到对应元素。

val1 := mt[1]
val2 := mt[2]
val3 := mt[3]

由于 key 可以起到唯一标识元素的作用,如果在设置元素时多次使用同一个 key,那么新设置的元素值会替换旧的值。例如:

var xm = map[int32][]byte{}
xm[21] = []byte("c7g59rof7ij5")
xm[43] = []byte("xyxyxy")
fmt.Printf("第一次赋值(key: 43): %v\n", xm[43])

xm[43] = []byte("dkdkdk")
fmt.Printf("第二次赋值(key: 43): %v\n", xm[43])

在上面代码中,key 为 43 的元素设置了两次,最终映射对象会保留第二次赋值的内容,丢弃第一次所赋值的内容。代码运行结果如下:

第一次赋值(key: 43):[120 121 120 121 120 121]    // 此值会被替换
第二次赋值(key: 43):[100 107 100 107 100 107]  // 此值被保留

要枚举映射对象中的所有元素,应当使用带 range 子句的 for 循环。

// 初始化映射对象
myMap := map[string]int {
    "task-01": 1000,
    "task-02": 1001,
    "task-03": 1002,
    "task-04": 1003,
    "task-05": 1004,
}

for key, val := range myMap {
    fmt.Printf("key: %s\tvalue: %d\n", key, val)
}

range 子句枚举出来的元素包含两个值——元素的 key 和元素的值。输出结果如下:

key: task-01  value: 1000
key: task-02  value: 1001
key: task-03  value: 1002
key: task-04  value: 1003
key: task-05  value: 1004

检查 key 的存在性

如果要访问的某个 key 在映射对象中不存在,会返回元素类型的默认值。

var m = map[string]int{}
m["c1"] = 7728
m["c4"] = 7729

// 访问 key 为 c3 的元素
// 此 key 不存在
xv := m["c3"]

“c3”在映射对象 m 中不存在,使得 m[“c3”]获取的是 int 类型的默认值 0。许多时候,获取不存在元素的值没有实际意义。因此,在访问元素前检查一下其是否存在很有必要。不妨将上述代码做以下修改:

xv, ok := m["c3"]
if ok {
    fmt.Printf("c3: %d\n", xv)
} else {
    fmt.Println("c3 不存在")
}

实际上就是在获取元素时多使用了一个变量(上面代码中的 ok),该变量的类型为 bool,如果要访问的 key 存在于映射对象中,则 ok 为 true,否则为 false。

双向链表

在双向链表中,每个元素都包含两个指针——分别指向前一个元素和后一个元素。在双向链表中,随机取出一个元素都能找到它前面的或者后面的元素。

可以在双向链表的任意位置插入新元素,也可以在链表内部移动元素的位置。

与双向链表有关的 API

container/list 包公开了一系列用于操作双向链表的 API,接下来将一一进行介绍。

首先是 Element 结构体。它表示链表中一个元素,源代码如下:

type Element struct {
    // 指向前、后元素的指针
    next, prev *Element

    // 此元素所属的链表对象
    list *List

    // 此元素中所存储的值
    Value interface{}
}

其中 Value 字段表示该元素的值。另外,还有三个未公开的字段:prev 表示指向上一个元素的指针,next 表示指向下一个元素的指针,list 表示指向当前链表的指针(即此元素所属的链表对象)。Element 结构体还包含以下方法:

  1. Next:返回与此元素链接的下一个元素。
  2. Prev:返回与此元素链接的上一个元素。

其次是 List 结构体,它表示链表对象,其源代码如下:

type List struct {
    root Element  // 此元素仅作为占位符使用,在代码中不直接访问
    len  int      // 链表的长度,即包含元素的个数
}

root 字段只是一个占位符,外部代码无法访问。在初始化链表对象时,prev 指针和 next 指针会引用 root 自身。当链表中插入元素后,root 字段可作为首部和尾部的分隔符。从链表首部插入的元素被 root.next 指针引用,从链表尾部插入的元素被 root.prev 指针引用。

List 结构体公开了以下方法:

  1. PushFront:把新元素插入到链表的头部。
  2. PushBack:把新元素插入到链表的尾部。
  3. InsertBefore:把新元素插入到指定元素的前面。
  4. InsertAfter:把新元素插入到指定元素的后面。
  5. MoveBefore:把一个元素移动到另一个元素的前面。
  6. MoveAfter:把一个元素移动到另一个元素的后面。
  7. MoveToFront:把某个元素移动到链表的首位。
  8. MoveToBack:把某个元素移动到链表的末位。
  9. Remove:从链表中删除指定的元素。
  10. Front:获取链表中的第一个元素。
  11. Back:获取链表中的最后一个元素。
  12. Len:获取链表的长度(包含元素的个数)。
  13. PushFrontList:复制另一个链表实例中的元素,并插入到当前链表实例的头部。
  14. PushBackList:复制另一个链表实例中的元素,并插入到当前链表实例的尾部。

list 还公开了一个 New 函数,其功能是创建一个双向链表实例。函数定义如下:

func New() *List

创建链表实例

创建链表实例最简单的方法就是调用 New 函数。

var mylist = list.New()

New 函数所返回的类型为指向 List 实例的指针。这可以保证在存入或删除元素时操作的是同一个链表实例。如果存入元素的链表与取出元素的链表不是同一个实例,那么数据结构就会被破坏,失去实际意义。

New 函数的源代码如下:

func New() *List { return new(List).Init() }

在实例化 List 对象过程中,New 函数做了两件事:

  1. 调用 new 函数(此处 new 函数是内置函数,非 list 包中的 New 函数)为 List 实例分配内存空间,并返回引用该地址的指针。
  2. 得到 List 实例的指针后,调用 Init 方法,初始化空链表。

Init 方法的源代码如下:

func (l *List) Init() *List {
    l.root.next = &l.root
    l.root.prev = &l.root
    l.len = 0
    return l
}

此方法让 List 内部的 root 元素(此元素仅作为占位符使用,非实际元素)的 prev、next 指针都引用自己,形成一个闭环,并把链表的长度设置为 0,如下图。

假设链表中插入了元素 A,那么 root 元素对自身的引用闭环被打开,加入了元素 A。由于链表中只有元素 A(链表长度为 1),所以不管是从链表的首部插入,还是从尾部插入,元素 A 的位置都一样,如下图。

在链表的尾部插入元素 B、C 后,得到的链表如下图所示:

注意这里末位元素 C 与 root 元素之间也是首尾相接的,但 root 元素仅作为链表开头与结尾的标志,不能作为实际元素来访问。

添加和删除元素

双向链表可以用四种方式来添加元素:

  1. 插入元素到链表首部。
  2. 追加元素到链表末尾。
  3. 在某个元素之前插入元素。
  4. 在某个元素之后插入元素。

下面通过一个完整的示例来加以演示。

步骤 1:定义 printElems 函数,用于向屏幕输出某个链表实例的所有元素。

func printElems(ls *list.List) {
    for x := ls.Front(); x != nil; x = x.Next() {
        fmt.Printf("%v ", x.Value)
    }
    fmt.Print("\n")
}

步骤 2:初始化双向链表实例。

var mylist = list.New()

步骤 3:在链表首部添加一个元素。

mylist.PushFront(100)

步骤 4:在链表的末尾再添加三个元素,然后输出一次链表中的元素(调用 printElems 函数)。

mylist.PushBack(200)
mylist.PushBack(300)
mylist.PushBack(400)
fmt.Print("此时链表中有四个元素:")
printElems(mylist)

步骤 5:在链表的首部再插入一个元素,然后再输出一遍链表的元素。

mylist.PushFront(90)
fmt.Print("\n在首部插入元素:")
printElems(mylist)

步骤 6:在倒数第二个元素的前面插入一个元素。

last := mylist.Back()       // 最后一个元素
x := last.Prev()            // 倒数第二个元素
mylist.InsertBefore(700, x)

步骤 7:在第一个元素的后面插入元素。

first := mylist.Front()     // 第一个元素
mylist.InsertAfter(800, first)

步骤 8:运行示例代码,得到的结果如下:

此时链表中有四个元素:100 200 300 400
在首部插入元素:90 100 200 300 400
在倒数第二个元素前插入元素:90 100 200 700 300 400
在第一个元素后插入元素:90 800 100 200 700 300 400

PushFront、PushBack、InsertBefore、InsertAfter 等方法在调用后会返回指向新元素的指针。如果需要在代码中访问这些元素,可以定义变量去接收这些返回值。

要删除某个元素,可以调用 Remove 方法。调用后,指定的元素将从链表中删除,并且返回被删除元素的值。

下面代码中,先向链表添加四个元素,然后逐一删除。

// 初始化双向链表实例
theList := list.New()

// 添加四个元素
e1 := theList.PushBack("a-b-c-d")
e2 := theList.PushBack("h-i-j-k")
e3 := theList.PushBack(-5000)
e4 := theList.PushBack(-3000)

// 即将删除元素
fmt.Printf("链表中已存元素个数:%d\n", theList.Len())

// 删除第一个元素
val1 := theList.Remove(e1)
fmt.Printf("删除%v后,还剩%d个元素\n", val1, theList.Len())

// 删除第二个元素
val2 := theList.Remove(e2)
fmt.Printf("删除%v后,还剩%d个元素\n", val2, theList.Len())

// 删除第三个元素
val3 := theList.Remove(e3)
fmt.Printf("删除%v后,还剩%d个元素\n", val3, theList.Len())

// 删除第四个元素
val4 := theList.Remove(e4)
fmt.Printf("删除%v后,还剩%d个元素\n", val4, theList.Len())

上述代码中,由于调用 Remove 方法时需要引用已存入链表的四个元素,所以在调用 PushBack 方法时定义新的变量来接收新元素的指针。

运行结果如下:

链表中已存元素个数:4
删除a-b-c-d后,还剩3个元素
删除h-i-j-k后,还剩2个元素
删除-5000后,还剩1个元素
删除-3000后,还剩0个元素

如果在调用 PushBack 方法时没有定义新的变量来接收新元素的指针,也可以通过元素的链接关系来删除。

假设链表存放有元素:A、B、C、D、E、F。以下两种方案均可以删除所有元素:

  1. 从前向后删除:调用 Front 方法获取 A 的引用,删除 A 后,剩下 B、C、D、E、F,再调用 Front 方法获取 B 的引用(此时链表中的第一个元素是 B),然后把 B 删除,通过 Front 方法获取 C 的引用,然后删除 C……直到 F 被删除。
  2. 从后向前删除:调用 Back 方法获取链表中 F 的引用(最后一个元素),将 F 删除,再通过 Back 方法获取 E 的引用(此时链表中最后一个元素是 E),将 E 删除,再调用 Back 方法获取 D 的引用,然后删除 D……直到 A 被删除。

下面的代码初始化了一个双向链表对象,然后添加五个元素。

var myls = list.New()
// 添加五个元素
// -1, -2, -3, -4, -5
myls.PushFront(-5)
myls.PushFront(-4)
myls.PushFront(-3)
myls.PushFront(-2)
myls.PushFront(-1)

从前向后逐一删除元素。

for e := myls.Front(); e != nil; e = myls.Front() {
    tmp := myls.Remove(e)
    fmt.Printf("已删除%v\n", tmp)
}

执行代码后,输出结果如下:

已删除 -1
已删除 -2
已删除 -3
已删除 -4
已删除 -5

也可以从后向前删除元素。

for e := myls.Back(); e != nil; e = myls.Back() {
    tmp := myls.Remove(e)
    fmt.Printf("已删除%v\n", tmp)
}

输出结果如下:

已删除 -5
已删除 -4
已删除 -3
已删除 -2
已删除 -1

移动元素

在双向链表中,元素有四种移动方式:

  1. 移到链表的首位(最前)。
  2. 移到链表的末位(最后)。
  3. 移到某个元素的前面。
  4. 移到某个元素的后面。

下面示例将通过移动元素的方法来反转链表中的元素顺序。

步骤 1:初始化链表实例。

var ls = list.New()

步骤 2:向链表添加五个元素。

ls.PushBack(1)
ls.PushBack(2)
ls.PushBack(3)
ls.PushBack(4)
ls.PushBack(5)

步骤 3:获取第一、二、四、五个元素的引用。在反转元素顺序过程中,第三个元素的位置不需要改变,因此没有必要获取第三个元素的引用。

// 获取第一个、最后一个元素的引用
first, last := ls.Front(), ls.Back()

// 获取第二个、第四个元素的引用
second, fourth := first.Next(), last.Prev()

步骤 4:将第二个元素移动到第四位,即最后一个元素之前。

ls.MoveBefore(second, last)

步骤 5:将第四个元素移到第二位,即第一个元素的后面。

ls.MoveAfter(fourth, first)

步骤 6:将第一个和最后一个元素的位置互换。第一个元素移到链表的末尾,最后一个元素移到链表的首部。

ls.MoveToFront(last)
ls.MoveToBack(first)

步骤 7:链表原来的元素顺序如下:

1 2 3 4 5

步骤 8:移动元素后,链表中的元素顺序如下:

5 4 3 2 1

枚举链表元素

List 对象不能使用 for…range 语句来枚举元素,因为它与数组、切片不同。链表中的元素是通过 prev、next 两个指针的引用关系来确定元素顺序的。

不过,枚举链表元素是可以使用 for 语句的,实现思路有两种:

  1. 调用链表实例的 Front 方法获得第一个元素的引用,然后通过元素的 Next 方法获取第二个元素的引用,再通过第二个元素的 Next 方法获取第三个元素的引用……直到到达链表的尾部(Next 方法返回 nil,即空指针)。
  2. 调用链表实例的 Back 方法获取最后一个元素的引用,然后通过 Prev 方法获取倒数第二个元素的引用,再通过倒数第二个元素的 Prev 方法获取倒数第三个元素的引用……直到到达链表的首部(Prev 方法返回 nil)。

下面代码分别演示两种枚举方法。

// 初始化双向链表
var lst = list.New()

// 向链表添加元素
lst.PushBack(1000)
lst.PushBack(2000)
lst.PushBack(3000)
lst.PushBack(4000)
lst.PushBack(5000)
lst.PushBack(6000)

// 第一种枚举方法:头 --> 尾
fmt.Print("从头到尾枚举:")
for e := lst.Front(); e != nil; e = e.Next() {
    fmt.Printf("%v ", e.Value)
}
fmt.Print("\n\n")

// 第二种枚举方法:尾 --> 头
fmt.Print("从尾到头枚举:")
for e := lst.Back(); e != nil; e = e.Prev() {
    fmt.Printf("%v ", e.Value)
}
fmt.Print("\n")

运行结果如下:

从头到尾枚举:1000 2000 3000 4000 5000 6000
从尾到头枚举:6000 5000 4000 3000 2000 1000
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值