571
社区成员
发帖
与我相关
我的任务
分享定义链表节点结构体本内容(位于 LinkTable.go 中):
// 链表节点的值类型
type ValType = int
// 链表节点
type Node struct {
val ValType // 节点值
prev *Node // 前节点
next *Node // 后节点
}
func NewNode(v ValType) *Node {
return &Node{v, nil, nil}
}
我将 LinkTable 定义为双向链表,因此每个节点中包含前向指针和后向指针。
由于不太了解 Go 如何更好实现泛型,我采用类型别名的方式将节点的值类型定义为 ValType,暂时实现为 int类型。
然后节点的构造利用工厂方法 NewNode 完成,前向指针和后向指针初始为空指针。
为了实现接口与实现分离,首先定义 LinkTable 接口,该接口定义了的所有需要完成的功能接口。本内容位于 LinkTable.go 中。
Go 接口的语法如下:
/* 接口语法 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
LinkTable 接口定义如下:
// 链表接口设计
type LinkTable interface {
/* 基本操作 */
Empty() bool
Size() int
Head() (*Node, error) // 获取头节点
Tail() (*Node, error) // 获取尾节点
Clear() // 清空链表
ToString() string
/* 插入节点 */
Insert(*Node) // 在尾节点后插入给定节点
InsertVal(ValType) // 插入一个给定值的节点
InsertBefor(node1 *Node, node2 *Node) error // 在 node1 后插入 node2
InsertAfter(node1 *Node, node2 *Node) error // 在 node1 前插入 node2
/* 删除节点 */
Delete(*Node) error
DeleteFirst(ValType) error // 删除第一个等于给定值的节点
/* 查找节点 */
FindFirst(ValType) (*Node, error) // 返回第一个等于给定值的节点
}
包括如下几类功能模块:
基本功能函数
包括获取链表长度、头尾节点等基本操作。为了方便打印 LinkTable 的内容,我还实现了一个 ToString 方法,方便调试测试时输出链表中节点值。
插入节点
实现了四种不同接口,分别是:按指针插入,按值插入,给定节点前插入,给定节点后插入。
删除节点
实现了两种不同接口,分别是:按指针删除,删除从头到尾首个等于给定值的节点。
查找节点
查找从头到尾首个等于给定值的节点。
注意事项:
Head()获取头节点时将返回非空错误码。Go 实现面向对象设计的方式与 python 相似,都是采用所谓 "duck type" 的设计哲学。具体来说,就是只要一个类实现了某接口中定义的所有方法,就可以称这个类实现该接口。
在 Go 中结构体即为类,后面不做区分。我们需要完成的就是定义一个类,实现上节所述的所有接口方法。
我们将具体实现类定义为 ConcretLinkTable,其数据成员如下:
/* 定义具体实现 */
type ConcretLinkTable struct {
count int // 节点数
head *Node // 头节点
tail *Node //尾节点
}
注意到我们使用 count 来统计节点个数以便实现 O(1) 的长度计算,故在插入和删除节点操作时,我们需要维护该计数的正确性。
head 和 tail 分别指向链表的头节点和尾节点。当链表为空时,头节点和尾节点都为 nil。头节点的前向指针和尾节点的后向指针永远为 nil。
Go 的方法与普通函数相比,虽然也是定义在结构体外面,但其语法定义有所不同,因为我们需要额外传递 ”this“ 指针参数,其语法如下:
func (this_name data_type) function_name() [return_type]{
/* 方法体*/
}
其特殊之处在于,func关键字后面额外多了一个变量定义,该变量即为我们的类对象 (C++ 中的 this)。对象方法的调用采用常见的obj.func()的形式。
接下来看具体的方法实现(位于 ConcretLinkTable.go 中):
基本函数
func (lt *ConcretLinkTable) Empty() bool {
return lt.head == nil
}
func (lt *ConcretLinkTable) Size() int {
return lt.count
}
func (lt *ConcretLinkTable) Head() (*Node, error) {
if !lt.Empty() {
return lt.head, nil
}
return nil, errors.New("LinkTable: No head node exists!")
}
func (lt *ConcretLinkTable) Tail() (*Node, error) {
if !lt.Empty() {
return lt.tail, nil
}
return nil, errors.New("LinkTable.Tail: No head node exists!")
}
func (lt *ConcretLinkTable) Clear() {
lt.count = 0
lt.head = nil
lt.tail = nil
}
func (lt *ConcretLinkTable) ToString() string {
s := "LinkTable: [ "
if !lt.Empty() {
for cur := lt.head; cur != lt.tail; cur = cur.next {
s += fmt.Sprintf("%d, ", cur.val)
}
s += fmt.Sprintf("%d", lt.tail.val)
}
s += " ]"
return s
}
基本功能函数的实现较为简单,只是需要注意错误处理即可。
插入节点
/* 插入节点 */
func (lt *ConcretLinkTable) Insert(node *Node) {
if lt.Empty() { // 原链表为空
node.prev = nil
lt.head = node
} else {
node.prev = lt.tail
lt.tail.next = node
}
lt.tail = node
lt.tail.next = nil
lt.count++ // 节点数自增
}
func (lt *ConcretLinkTable) InsertVal(val ValType) {
lt.Insert(&Node{val, nil, nil})
}
// 注意:node1 应该是 lt 中的节点!
func (lt *ConcretLinkTable) InsertBefor(node1 *Node, node2 *Node) error {
if node1 != nil && node2 != nil {
node2.prev = node1.prev
node1.prev = node2
node2.next = node1
if node2.prev != nil {
node2.prev.next = node2
}
lt.count++
return nil
}
return errors.New("LinkTable.InsertBefor: node is nil!")
}
// 注意:node1 应该是 lt 中的节点!
func (lt *ConcretLinkTable) InsertAfter(node1 *Node, node2 *Node) error {
if node1 != nil && node2 != nil {
node2.next = node1.next
node1.next = node2
node2.prev = node1
if node2.next != nil {
node2.next.prev = node2
}
lt.count++
return nil
}
return errors.New("LinkTable.InsertAfter: node is nil!")
}
注意事项:
成功插入节点后需要将 count 自增
注意向空链表插入首个节点的边界条件,需要对头尾指针进行特殊处理
InsertVal(按值插入)的实现可以直接借助Insertvet(按节点插入)间接完成
对于InsertBefor,InsertAfter需要特别注意的是,第一个参数节点必须是该链表中的节点,否则将会导致异常行为。举个例子(伪码表示):
L1: [1,2,3], L2: [3,4,5]
L1.size() == 3, L2.size() == 3
L1.InsertAfter(L2.tail, node(6)) // 异常行为
L1: [1,2,3], L2: [3,4,5,6]
L1.size == 4(), L2.size() == 3
我们在链表对象 L1 上调用 InsertAfter 之后,出现诡异的结果:L1的实际结构没变,L2却多了一个节点,但我们的 count 计数却是在 L1 上进行了自增。
虽然这样设计很不好,对使用者带来负担,但我不知道该如何禁止这种异常的出现,而且 C++ STL 中的 list.insert 也会出这种情况。因此,虽然我们采取了一定的错误检测机制,但仍然可能出现异常情况。
删除节点
/* 删除节点 */
// 注意:node 应该是 lt 中的节点!
func (lt *ConcretLinkTable) Delete(node *Node) error {
if node != nil {
if node.prev != nil {
node.prev.next = node.next
} else { // node 为头节点
lt.head = lt.head.next
}
if node.next != nil {
node.next.prev = node.prev
} else { // node 为尾节点
lt.tail = lt.tail.prev
}
lt.count--
return nil
}
return errors.New("LinkTable.Delete: node is nil!")
}
func (lt *ConcretLinkTable) DeleteFirst(val ValType) error {
cur, err := lt.FindFirst(val)
if err == nil {
lt.Delete(cur)
return nil
}
return errors.New("LinkTable.DeleteFirst: No node with the given value!")
}
注意事项
Delete的参数为本链表中的节点。查找节点
/* 查找节点 */
func (lt *ConcretLinkTable) FindFirst(val ValType) (*Node, error) {
cur := lt.head
for i := 0; i < lt.count; i++ {
if cur.val == val {
return cur, nil
}
cur = cur.next
}
return nil, errors.New("LinkTable.FindFirst: No node with the given value!")
}
注意事项:
Go 集成了testing测试模块,定义了了一些测试用例,完成单元测试。
testing使用注意事项:
测试文件名必须以_test尾后缀(大小写敏感),否则无法被识别
测试用例函数名均以Test为前缀,形式为:func TestLinkTable(t *testing.T)
测试断言函数包括 Error/ErrorF 和 Fatal/FatalF,前者为非致命错误,检测到错误后仍继续执行其它测试用例;后者为致命错误,检测到后直接终止。
注意所有测试用例的执行顺序是不确定的,因此不要在测试用例之间存在顺序依赖。
测试执行命令:go test -v
测试示例如下(完整内容位于 LinkTable_test.go 中):
// Insert
for _, num := range nums {
lt.InsertVal(num)
}
if lt.ToString() != "LinkTable: [ 2, 1, 2, 2, 5, 3, 5, 7 ]" {
t.Error(lt.ToString())
}
if lt.Size() != 8 {
t.Error(lt.Size())
}
最终测试结果,测试用例均通过:
为了使得程序具有较好的扩展性,遵循面向接口编程原则,将接口与实现相分离。
接口的定义需要考虑使用的易用性与实现的复杂性。
实现时需要考虑到注意错误的检测处理,以及一些边界条件(例如插入节点到空链表)的处理。
最后还需要进行一些必要的单元测试以验证我们的实现的正确性。
待改进之处:
文件目录:
仓库地址:https://github.com/csmyx/LearnGo
作者:357