Go-数据结构与算法学习指南-全-

Go 数据结构与算法学习指南(全)

原文:zh.annas-archive.org/md5/4c0cad9e4d8ed148ba3cdbe40d759bf6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

使用 Go 学习数据结构和算法 涵盖了计算机编程中的简单和高级概念。主要目标是选择正确的算法和数据结构来解决问题。本书解释了比较算法复杂性和数据结构的概念,这些概念涉及代码性能和效率。

Golang 在过去两年中一直是热门词汇,这一领域取得了巨大的进步。许多开发者和组织正在逐渐迁移到 Golang,采用其快速、轻量级和内置的并发功能。这意味着我们需要在这个不断发展的语言中有一个扎实的数据结构和算法基础。

本书面向对象

这本全面的书是为那些想要了解如何选择最佳数据结构和算法以帮助解决特定问题的开发者而编写的。一些基本的 Go 编程知识将是一个额外的优势。

这本书是为那些想要学习如何编写高效程序并使用适当的数据结构和算法的人而编写的。

为了充分利用这本书

我们假设的知识是关于矩阵、集合操作和统计概念等主题的基本编程语言和数学技能。读者应该具备根据流程图或指定算法编写伪代码的能力。编写功能性代码、测试、遵循指南以及在 Go 语言中构建复杂项目是我们假设的读者技能的先决条件。

下载示例代码文件

您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com 登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载和勘误表。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本的软件解压缩或提取文件夹:

  • WinRAR/7-Zip 用于 Windows

  • Zipeg/iZip/UnRarX 用于 Mac

  • 7-Zip/PeaZip 用于 Linux

该书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789618501_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText: 表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“让我们在下一节中看看len函数。”

代码块设置为如下:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package
import (
  "fmt"
)
// main method
func main() {
  fmt.Println("Hello World")
}

任何命令行输入或输出都写作如下:

go build
./hello_world

粗体: 表示新术语、重要词汇或您在屏幕上看到的词汇。

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com发送邮件给我们。

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packt.com

第一部分:数据结构和算法及 Go 语言简介

我们将介绍抽象数据类型、定义和数据的分类。读者在阅读这部分内容后,将对算法的性能分析以及为结构设计模式选择合适的数据结构有很好的了解。

本节包含以下章节:

  • 第一章,数据结构和算法

  • 第二章,Go 语言数据结构和算法入门

第一章:数据结构和算法

数据结构是数据的组织方式,旨在减少存储空间的使用,并降低执行不同任务时的难度。数据结构用于处理和操作大量数据,在各个领域都有应用,例如数据库管理和互联网索引服务。

在本章中,我们将重点关注抽象数据类型的定义,将数据结构分类为线性、非线性、同构、异构和动态类型。本章介绍了如容器、列表、集合、映射、图、栈和队列等抽象数据类型。我们还将涵盖数据结构的性能分析、选择合适的数据结构和结构设计模式。

读者可以使用 Go 中的正确数据结构开始编写基本算法。给定一个问题,选择数据结构和不同的算法将是第一步。之后,进行性能分析将是下一步。不同算法的时间和空间分析有助于比较它们,并帮助您选择最优的算法。要开始,对 Go 的基本知识是必要的。

在本章中,我们将涵盖以下主题:

  • 数据结构和结构设计模式的分类

  • 算法的表示

  • 复杂度和性能分析

  • 暴力算法

  • 分而治之算法

  • 回溯算法

技术要求

golang.org/doc/install为您的操作系统安装 Go 版本 1.10。

本章的代码文件可以在以下 GitHub URL 找到:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter01.

通过运行位于github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/hello_world的 hello world 程序来检查 Go 的安装:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package
import (
  "fmt"
)
// main method
func main() {
  fmt.Println("Hello World")
}

运行以下命令:

go build
./hello_world

以下截图显示了输出:

图片

让我们看看下一节中数据结构和结构设计模式的分类。

数据结构和结构设计模式的分类

您可以通过分类来选择数据结构。在本节中,我们将详细讨论数据结构的分类。在分类之后,我们将介绍与数据结构相关的模式设计。

在下一节中,我们将探讨数据结构的分类。

数据结构的分类

数据结构这个术语指的是在计算机内存中组织数据,以便快速检索以进行处理。它是一种数据组织方案,将数据结构的函数定义与其实现解耦。数据结构的选择基于问题类型和数据上的操作。

如果数据结构需要各种数据类型,我们可以选择异构数据结构。链表、有序列表和无序列表被分组为异构数据结构。线性数据结构包括列表、集合、元组、队列、栈和堆。树、表和容器被归类为非线性数据结构。二维和多维数组被分组为同构数据结构。动态数据结构包括字典、树集和序列。

数据结构的分类如下所示:

让我们在下一节中看看列表、元组和堆。

列表

列表是元素的序列。每个元素可以通过链接与前一个或后一个元素连接。元素可以具有其他有效载荷属性。这种数据结构是基本容器类型。列表具有可变长度,开发者可以比数组更容易地删除或添加元素。列表中的数据项在内存或磁盘上不需要连续。链表是由 RAND 公司的 Allen Newell、Cliff Shaw 和 Herbert A. Simon 提出的。

要开始,可以使用 Go 中的列表,如下例所示;元素通过列表上的PushBack方法添加,该方法是container/list包的一部分:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt and container list packages
import (
   "fmt"
   "container/list")

// main method
func main() {
    var intList list.List
    intList.PushBack(11)
    intList.PushBack(23)
    intList.PushBack(34)

    for element := intList.Front(); element != nil; element=element.Next() {
        fmt.Println(element.Value.(int))
    }
}

列表可以通过for循环迭代,并通过Value方法访问元素的值。

运行以下命令:

go run list.go

以下截图显示了输出:

让我们在下一节中看看元组。

元组

元组是元素有限排序的列表。它是一种将数据分组的数据结构。元组通常是不可变的顺序集合。元素具有不同数据类型的关联字段。修改元组的唯一方法是更改字段。+ 和 * 等运算符可以应用于元组。数据库记录被称为元组。在以下示例中,计算了整数的幂级数,并返回整数的平方和立方作为元组:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package
import (
  "fmt"

)
//gets the power series of integer a and returns tuple of square of a
// and cube of a
func powerSeries(a int) (int,int) {

  return a*a, a*a*a

}

main方法使用3作为参数调用powerSeries方法。方法返回squarecube值:

// main method
func main() {

  var square int
  var cube int
  square, cube = powerSeries(3)

  fmt.Println("Square ", square, "Cube", cube)

}

运行以下命令:

go run tuples.go

以下截图显示了输出:

powerSeries函数中,可以为元组命名,如下面的代码所示:

func powerSeries(a int) (square int, cube int) {

  square = a*a

  cube = square*a

  return 

}

如果出现错误,它可以与元组一起传递,如下面的代码所示:

func powerSeries(a int) (int, int, error) {

  square = a*a

  cube = square*a

  return square,cube,nil

}

堆是一种基于 heap 属性的数据结构。堆数据结构用于选择、图和 k-路归并算法。在堆上执行查找、合并、插入、键更改和删除等操作。堆是 Go 中的 container/heap 包的一部分。根据堆顺序(最大堆)属性,每个节点存储的值大于或等于其子节点。

如果是降序排列,则称为最大堆;否则,是最小堆。堆数据结构由 J.W.J. Williams 在 1964 年提出,用于堆排序算法。它不是一个排序数据结构,但部分有序。以下示例展示了如何使用 container/heap 包创建堆数据结构:

//main package has examples shown
//in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package and container/heap
import (
  "container/heap"
  "fmt"
)
// integerHeap a type
type IntegerHeap []int

// IntegerHeap method - gets the length of integerHeap
func (iheap IntegerHeap) Len() int { return len(iheap) }

// IntegerHeap method - checks if element of i index is less than j index
func (iheap IntegerHeap) Less(i, j int) bool { return iheap[i] < iheap[j] }
// IntegerHeap method -swaps the element of i to j index
func (iheap IntegerHeap) Swap(i, j int) { iheap[i], iheap[j] = iheap[j], iheap[i] }

IntegerHeap 拥有一个 Push 方法,该方法使用以下接口来推送项目:


//IntegerHeap method -pushes the item
func (iheap *IntegerHeap) Push(heapintf interface{}) {

 *iheap = append(*iheap, heapintf.(int))
}
//IntegerHeap method -pops the item from the heap
func (iheap *IntegerHeap) Pop() interface{} {
 var n int
 var x1 int
 var previous IntegerHeap = *iheap
 n = len(previous)
 x1 = previous[n-1]
 *iheap = previous[0 : n-1]
 return x1
}

// main method
func main() {
 var intHeap *IntegerHeap = &IntegerHeap{1,4,5}

 heap.Init(intHeap)
 heap.Push(intHeap, 2)
 fmt.Printf("minimum: %d\n", (*intHeap)[0])
 for intHeap.Len() > 0 {
 fmt.Printf("%d \n", heap.Pop(intHeap))
 }
}

运行以下命令:

go run heap.go

以下截图显示了输出:

让我们看看下一节的结构设计模式

结构设计模式

结构设计模式描述了实体之间的关系。它们用于使用类和对象形成大型结构。这些模式用于以灵活的方式创建具有不同系统块的系统。适配器、桥接、组合、装饰器、外观、享元、私有类数据和代理是 Gang of Four (GoF) 结构设计模式。私有类数据设计模式是本节中涵盖的另一个设计模式。

我们将在下一节中探讨适配器和桥接设计模式。

适配器

适配器模式提供了一个包装器,该包装器具有 API 客户端所需的接口,用于连接不兼容的类型,并在两种类型之间充当翻译器。适配器使用一个类的接口作为具有另一个兼容接口的类。当需求变化时,存在一些场景,由于不兼容的接口,需要更改类功能。

当一个类定义自己的接口到由 adapter 类实现的下一级模块接口时,可以通过使用适配器模式来遵循依赖倒置原则。委托是适配器模式使用的另一个原则。处理多种格式并执行源到目标转换的场景是应用适配器模式的场景。

适配器模式包括目标、适配体、适配器和客户端:

  • 目标是客户端调用的接口,在适配器和适配体上调用方法。

  • 客户端希望适配器实现不兼容的接口。

  • 适配器将适配体的不兼容接口转换为客户端想要的接口。

假设你有一个IProcessor接口,它有一个process方法,Adapter类实现了process方法,并且有一个Adaptee实例作为属性。Adaptee类有一个convert方法和一个adapterType实例变量。当开发者使用 API 客户端时,会调用process接口方法来在Adaptee上调用convert方法。代码如下:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
//IProcess interface
type IProcess interface {
 process()
}
//Adapter struct
type Adapter struct {
 adaptee Adaptee
}

Adapter类有一个process方法,它会在adaptee上调用convert方法:

//Adapter class method process
func (adapter Adapter) process() {
 fmt.Println("Adapter process")
 adapter.adaptee.convert()
}
//Adaptee Struct
type Adaptee struct {
 adapterType int
}
// Adaptee class method convert
func (adaptee Adaptee) convert() {
 fmt.Println("Adaptee convert method")
}
// main method
func main() {
var processor IProcess = Adapter{}
processor.process()
}

执行以下命令:

go run adapter.go

下面的截图显示了输出:

截图

让我们在下一节中看看桥接模式。

桥接模式

桥接模式将实现与抽象解耦。抽象基类可以被子类化以提供不同的实现,并允许轻松修改实现细节。作为桥接的接口有助于使具体类的功能独立于接口实现类。桥接模式允许在运行时更改实现细节。

桥接模式展示了原则,更倾向于组合而非继承。它有助于那些需要多次正交子类化的情况。应用运行时绑定、正交类层次映射和平台独立性实现是桥接模式可以应用的场景。

桥接模式组件包括抽象、细化抽象、实现者和具体实现者。抽象是作为抽象类实现的接口,客户端通过在具体实现者上调用方法来调用它。抽象与实现保持has-a关系,而不是is-a关系。has-a关系通过组合来维护。抽象有一个实现引用。细化抽象比抽象提供更多变体。

假设IDrawShape是一个接口,它有一个drawShape方法。DrawShape实现了IDrawShape接口。我们创建了一个带有drawContour方法的IContour桥接接口。轮廓类实现了IContour接口。ellipse类将具有abr属性和drawShapeDrawShape的一个实例)。ellipse类实现了contour桥接接口以实现drawContour方法。drawContour方法在drawShape实例上调用drawShape方法。

下面的代码演示了桥接实现:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
//IDrawShape interface
type IDrawShape interface {
 drawShape(x[5] float32,y[5] float32)
}
//DrawShape struct
type DrawShape struct{}

drawShape 方法

drawShape方法根据给定的坐标绘制形状,如下面的代码所示:

// DrawShape struct has method draw Shape with float x and y coordinates
func (drawShape DrawShape) drawShape(x[5] float32, y[5] float32) {
 fmt.Println("Drawing Shape")
}
//IContour interace
type IContour interface {
 drawContour(x[5] float32 ,y[5] float32)
 resizeByFactor(factor int)
}
//DrawContour struct
type DrawContour struct {
 x[5] float32
 y[5] float32
 shape DrawShape
 factor int
}

drawContour 方法

DrawContour类的drawContour方法在shape实例上调用drawShape方法,如下面的代码所示:

//DrawContour method drawContour given the coordinates
func (contour DrawContour) drawContour(x[5] float32,y[5] float32) {
 fmt.Println("Drawing Contour")
 contour.shape.drawShape(contour.x,contour.y)
}
//DrawContour method resizeByFactor given factor
func (contour DrawContour) resizeByFactor(factor int) {
 contour.factor = factor
}
// main method
func main() {
var x = [5]float32{1,2,3,4,5}
var y = [5]float32{1,2,3,4,5}
var contour IContour = DrawContour{x,y,DrawShape{},2}
contour.drawContour(x,y)
 contour.resizeByFactor(2)
}

执行以下命令:

go run bridge.go

下面的截图显示了输出:

截图

在下一节中,我们将探讨组合、装饰者、外观和享元设计模式。

组合

组合是一组相似对象的单个对象。对象以树形结构存储以持久化整个层次结构。组合模式用于更改对象的分层集合。组合模式基于异构集合。可以不更改接口和客户端代码添加新类型的对象。例如,可以使用组合模式进行 Web 上的 UI 布局、目录树以及跨部门管理员工。该模式提供了一种以类似方式访问单个对象和组的方法。

组合模式包括component接口、component类、组合和客户端:

  • component接口定义了所有对象默认行为以及访问组合组件的行为。

  • compositecomponent类实现了component接口。

  • 客户端通过组件接口与组件交互以调用组合中的方法。

假设有一个具有perform方法且实现IComposite接口的BranchClass,它有addLeafaddBranchperform方法。Leaflet类通过perform方法实现ICompositeBranchClassleafsbranches具有一对一的关系。通过递归遍历分支,可以遍历组合树,如下代码所示:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
// IComposite interface
type IComposite interface {
 perform()
}
// Leaflet struct
type Leaflet struct {
 name string
}
// Leaflet class method perform
func (leaf *Leaflet) perform() {
fmt.Println("Leaflet " + leaf.name)
}
// Branch struct
type Branch struct {
 leafs []Leaflet
 name string
 branches[]Branch
}

Branch类的perform方法调用branchleafs上的perform方法,如下代码所示:

// Branch class method perform
func (branch *Branch) perform() {
fmt.Println("Branch: " + branch.name)
 for _, leaf := range branch.leafs {
 leaf.perform()
 }
for _, branch := range branch.branches {
 branch.perform()
 }
}
// Branch class method add leaflet
func (branch *Branch) add(leaf Leaflet) {
 branch.leafs = append(branch.leafs, leaf)
}

如以下代码所示,Branch类的addBranch方法添加了一个新的branch

//Branch class method addBranch branch
func (branch *Branch) addBranch(newBranch Branch) {
branch.branches = append(branch.branches,newBranch)
}
//Branch class method getLeaflets
func (branch *Branch) getLeaflets() []Leaflet {
 return branch.leafs
}
// main method
func main() {
var branch = &Branch{name:"branch 1"}
var leaf1 = Leaflet{name:"leaf 1"}
var leaf2 = Leaflet{name:"leaf 2"}
var branch2 = Branch{name:"branch 2"}
branch.add(leaf1)
branch.add(leaf2)
branch.addBranch(branch2)
branch.perform()
}

运行以下命令:

go run composite.go

以下截图显示了输出:

图片

在下一节中,让我们看看装饰者模式。

装饰者

在类责任被移除或添加的场景中,应用装饰者模式。装饰者模式在修改功能时帮助进行子类化,而不是静态继承。一个对象可以有多个装饰器和运行时装饰器。可以使用装饰者实现单一职责原则。装饰者可以应用于窗口组件和图形对象建模。装饰者模式有助于在运行时修改现有实例属性和添加新方法。

装饰者模式参与者包括组件接口、具体组件类和decorator类:

  • 具体组件实现了组件接口。

  • decorator类实现了组件接口,并在相同的方法或额外的方法中提供附加功能。装饰器基类可以是一个参与者,代表所有装饰器的基类。

假设 IProcess 是一个具有 process 方法的接口。ProcessClass 实现了一个具有 process 方法的接口。ProcessDecorator 实现了过程接口并有一个 ProcessClass 的实例。ProcessDecorator 可以比 ProcessClass 添加更多功能,如下面的代码所示:

//main package has examples shown
 // in Hands-On Data Structures and algorithms with Go book
 package main
// importing fmt package
 import (
 "fmt"
 )
// IProcess Interface
 type IProcess interface {
 process()
 }
//ProcessClass struct
 type ProcessClass struct{}
//ProcessClass method process
 func (process *ProcessClass) process() {
 fmt.Println("ProcessClass process")
 }
//ProcessDecorator struct
 type ProcessDecorator struct {
 processInstance *ProcessClass
 }

在以下代码中,ProcessDecorator 类的 process 方法在 ProcessClass 的装饰器实例上调用 process 方法:


 //ProcessDecorator class method process
 func (decorator *ProcessDecorator) process() {
 if decorator.processInstance == nil {
 fmt.Println("ProcessDecorator process")
 } else {
 fmt.Printf("ProcessDecorator process and ")
 decorator.processInstance.process()
}
 }
//main method
 func main() {
var process = &ProcessClass{}
var decorator = &ProcessDecorator{}
decorator.process()
decorator.processInstance = process
decorator.process()
}

运行以下命令:

go run decorator.go

以下截图显示了输出:

图片

让我们看看下一节中的 Facade 模式。

门面

门面用于通过辅助工具抽象子系统接口。当接口数量增加且系统变得复杂时,使用门面设计模式。门面是不同子系统的入口点,它简化了系统之间的依赖关系。门面模式提供了一个接口,隐藏了隐藏代码的实现细节。

可以通过门面模式实现松耦合原则。你可以使用门面来改进设计不良的 API。在 SOA 中,可以使用服务门面来合并对合同和实现的更改。

门面模式由 facade 类、模块类和一个客户端组成:

  • 门面将客户端的请求委派给模块类。facade 类隐藏了子系统逻辑和规则的复杂性。

  • 模块类实现了模块子系统的行为和功能。

  • 客户端调用 facade 方法。facade 类的功能可以分散到多个包和组件中。

例如,账户、客户和交易是具有账户、客户和交易创建方法的类。BranchManagerFacade 可以被客户端用来创建账户、客户和交易:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
 )
 //Account struct
 type Account struct{
id string
accountType string
}
//Account class method create - creates account given AccountType
func (account *Account) create(accountType string) *Account{
 fmt.Println("account creation with type")
 account.accountType = accountType
return account
}
//Account class method getById given id string
func (account *Account) getById(id string) *Account {
 fmt.Println("getting account by Id")
 return account
 }

account 类有一个 deleteById 方法,用于删除具有给定 ID 的账户,如下面的代码所示:

 //Account class method deleteById given id string
 func (account *Account) deleteById(id string)() {
 fmt.Println("delete account by id")
 }
//Customer struct
 type Customer struct{
 name string
 id int
 }

在以下代码中,customer 类有一个创建带有 name 的新客户的方法:

//Customer class method create - create Customer given name
 func (customer *Customer) create(name string) *Customer {
 fmt.Println("creating customer")
 customer.name = name
 return customer
 }
//Transaction struct
 type Transaction struct{
 id string
 amount float32
 srcAccountId string
 destAccountId string
 }

如以下代码所示,transaction 类有一个用于创建交易的 create 方法:

//Transaction class method create Transaction
 func (transaction *Transaction) create(srcAccountId string, destAccountId string,amount float32) *Transaction {
 fmt.Println("creating transaction")
 transaction.srcAccountId = srcAccountId
 transaction.destAccountId = destAccountId
 transaction.amount = amount
 return transaction
 }
 //BranchManagerFacade struct
 type BranchManagerFacade struct {
 account *Account
 customer *Customer
 transaction *Transaction
 }
//method NewBranchManagerFacade
 func NewBranchManagerFacade() *BranchManagerFacade {
 return &BranchManagerFacade{ &Account{}, &Customer{}, &Transaction{}}
 }

BranchManagerFacade 有一个 createCustomerAccount 方法,它调用 customer 类实例上的 create 方法,如下面的代码所示:

//BranchManagerFacade class method createCustomerAccount
 func (facade *BranchManagerFacade) createCustomerAccount(customerName string, accountType string) (*Customer,*Account) {
 var customer = facade.customer.create(customerName)
 var account = facade.account.create(accountType)
 return customer, account
 }
 //BranchManagerFacade class method createTransaction
 func (facade *BranchManagerFacade) createTransaction(srcAccountId string, destAccountId string, amount float32) *Transaction {
var transaction = facade.transaction.create(srcAccountId,destAccountId,amount)
 return transaction
}

main 方法调用 NewBranchManagerFacade 方法来创建一个门面。在 facade 上的方法被调用以创建 customeraccount

//main method
func main() {
    var facade = NewBranchManagerFacade()
    var customer *Customer
    var account *Account
    customer, account = facade.createCustomerAccount("Thomas Smith", 
    "Savings")
    fmt.Println(customer.name)
    fmt.Println(account.accountType)
    var transaction = facade.createTransaction("21456","87345",1000)
    fmt.Println(transaction.amount)
}

运行以下命令:

go run facade.go

以下截图显示了输出:

图片

让我们看看下一节中的 Flyweight 模式。

Flyweight

轻量级模式用于管理具有高度变化的对象状态。该模式允许我们在多个对象之间共享对象状态中的公共部分,而不是每个对象都存储它。可变对象数据被称为外部状态,其余对象状态是内部状态。外部数据传递给轻量级方法,并且永远不会存储在其中。轻量级模式有助于减少整体内存使用和对象初始化开销。该模式有助于创建类间关系并降低内存到可管理的水平。

轻量级对象是不可变的。值对象是轻量级模式的一个好例子。轻量级对象可以在单线程模式下创建,确保每个值只有一个实例。在并发线程场景中,会创建多个实例。这是基于轻量级对象的等价标准。

轻量级模式的参与者是 FlyWeight 接口、ConcreteFlyWeightFlyWeightFactoryClient 类:

  • FlyWeight 接口有一个方法,通过该方法轻量级对象可以获取并作用于外部状态。

  • ConcreteFlyWeight 实现了 FlyWeight 接口以表示轻量级对象。

  • FlyweightFactory 用于创建和管理轻量级对象。客户端调用 FlyweightFactory 来获取轻量级对象。UnsharedFlyWeight 可以具有不共享的功能。

  • Client

假设 DataTransferObject 是一个具有 getId 方法的接口。DataTransferObjectFactory 通过 getDataTransferObject 方法根据 DTO 类型创建数据传输对象。DTO 类型包括客户、员工、经理和地址,如下代码所示:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
 package main
// importing fmt package
 import (
 "fmt"
 )
 //DataTransferObjectFactory struct
 type DataTransferObjectFactory struct {
 pool map[string] DataTransferObject
 }
//DataTransferObjectFactory class method getDataTransferObject
 func (factory DataTransferObjectFactory) getDataTransferObject(dtoType string) DataTransferObject {
var dto = factory.pool[dtoType]
if dto == nil {
fmt.Println("new DTO of dtoType: " + dtoType)
 switch dtoType{
 case "customer":
 factory.pool[dtoType] = Customer{id:"1"}
 case "employee":
 factory.pool[dtoType] = Employee{id:"2"}
 case "manager":
 factory.pool[dtoType] = Manager{id:"3"}
 case "address":
 factory.pool[dtoType] = Address{id:"4"}
 }
dto = factory.pool[dtoType]
}

 return dto
 }

在以下代码中,Customer 类实现了 DataTransferObject 接口:

// DataTransferObject interface
 type DataTransferObject interface {
 getId() string
 }
 //Customer struct
 type Customer struct {
 id string //sequence generator
 name string
 ssn string
 }
 // Customer class method getId
 func (customer Customer) getId() string {
 //fmt.Println("getting customer Id")
 return customer.id
}
 //Employee struct
 type Employee struct {
 id string
 name string
 }
 //Employee class method getId
 func (employee Employee) getId() string {
 return employee.id
 }
 //Manager struct
 type Manager struct {
 id string
 name string
 dept string
 }

如下代码所示,Manager 类实现了 DataTransferObject 接口:

//Manager class method getId
 func (manager Manager) getId() string {
 return manager.id
 }
 //Address struct
 type Address struct {
 id string
 streetLine1 string
 streetLine2 string
 state string
 city string
}
//Address class method getId
 func (address Address) getId() string{
 return address.id
 }
 //main method
 func main() {
 var factory = DataTransferObjectFactory{make(map[string]DataTransferObject)}
 var customer DataTransferObject = factory.getDataTransferObject("customer")
 fmt.Println("Customer ",customer.getId())
 var employee DataTransferObject = factory.getDataTransferObject("employee")
 fmt.Println("Employee ",employee.getId())
 var manager DataTransferObject = factory.getDataTransferObject("manager")
 fmt.Println("Manager",manager.getId())
 var address DataTransferObject = factory.getDataTransferObject("address")
 fmt.Println("Address",address.getId())
 }

执行以下命令:

go run flyweight.go

以下截图显示了输出:

我们将在下一节中查看私有类和代理数据模式。

私有类数据

私有类数据模式确保了类内部数据的安全。该模式封装了类数据的初始化。私有类属性中的写权限受到保护,属性在构造过程中被设置。私有类模式通过在保持状态的情况下将信息封装在类中来打印信息的暴露。类数据初始化的封装是此模式适用的一种场景。

Account 是一个包含账户详情和客户名称的类。AccountDetailsAccount 的私有属性,而 CustomerName 是公共属性。Account 的 JSON 序列化将 CustomerName 作为公共属性。AccountDetails 是 Go 中的包属性(模拟为私有类数据):

//main package has examples shown
 // in Hands-On Data Structures and algorithms with Go book
 package main
// importing fmt and encoding/json packages
import (
 "encoding/json"
 "fmt"
 )
 //AccountDetails struct
 type AccountDetails struct {
 id string
 accountType string
 }
 //Account struct
 type Account struct {
 details *AccountDetails
 CustomerName string
 }
 // Account class method setDetails
 func (account *Account) setDetails(id string, accountType string) {
account.details = &AccountDetails{id, accountType}
}

如以下代码所示,Account类有getId方法,它返回私有的类属性id

//Account class method getId
 func (account *Account) getId() string{
return account.details.id
 }
 //Account class method getAccountType
 func (account *Account) getAccountType() string{
return account.details.accountType
 }

main方法使用CustomerName调用Account初始化器。使用setDetails方法设置账户的详细信息:

// main method
 func main() {
var account *Account = &Account{CustomerName: "John Smith"}
 account.setDetails("4532","current")
jsonAccount, _ := json.Marshal(account)
 fmt.Println("Private Class hidden",string(jsonAccount))
fmt.Println("Account Id",account.getId())
fmt.Println("Account Type",account.getAccountType())
}

运行以下命令:

go run privateclass.go

以下截图显示了输出:

让我们看看下一节中的代理模式。

代理

代理模式将请求转发到真实对象,并作为其他对象的接口。代理模式控制对对象的访问并提供附加功能。附加功能可以与身份验证、授权和提供对资源敏感对象的访问权限相关。在提供附加逻辑时,不需要修改真实对象。远程、智能、虚拟和保护代理是应用此模式的情况。它还用于通过继承和对象组合提供扩展功能的替代方案。代理对象也被称为代表、处理程序或包装器。

代理模式包括主题接口、RealSubject类和Proxy类:

  • 主题是RealObjectProxy类的接口。

  • RealSubject对象在Proxy类中被创建并维护为引用。RealSubject是资源敏感的,需要被保护,且创建成本高昂。RealObject是一个实现IRealObject接口的类。它有一个performAction方法。

  • VirtualProxy用于访问RealObject并调用performAction方法。

以下代码展示了代理模式的实现:


 //main package has examples shown
 // in Hands-On Data Structures and algorithms with Go book
 package main
// importing fmt package
 import (
 "fmt"
 )
 //IRealObject interface
 type IRealObject interface {
 performAction()
 }
 //RealObject struct
 type RealObject struct{}
RealObject class implements IRealObject interface. The class has method performAction.
 //RealObject class method performAction
 func (realObject *RealObject) performAction() {
 fmt.Println("RealObject performAction()")
 }
 //VirtualProxy struct
 type VirtualProxy struct {
 realObject *RealObject
 }
 //VirtualProxy class method performAction
 func (virtualProxy *VirtualProxy) performAction() {
 if virtualProxy.realObject == nil {
 virtualProxy.realObject = &RealObject{}
 }
 fmt.Println("Virtual Proxy performAction()")
 virtualProxy.realObject.performAction()
 }
 // main method
 func main() {
 var object VirtualProxy = VirtualProxy{}
 object.performAction()
 }

运行以下命令:

go run virtualproxy.go

以下截图显示了输出:

现在我们已经了解了数据结构的分类和所使用的模式,让我们继续看看算法的表示。

算法的表示

流程图和伪代码是表示算法的方法。算法显示了解决问题逻辑。流程图有不同的表示符号,如入口、退出、任务、输入/输出、决策点和交互块。结构化程序由一系列这些符号组成,以执行特定任务。伪代码有文档、动作和流程控制关键字来可视化算法。文档关键字是任务备注设置放置获取是动作关键字。

让我们看看算法的不同表示,即在下一节中讨论的流程图和伪代码。

流程图

流程控制关键字是 SETLOOP、(WHILEUNTIL)、REPPOST。以下流程图显示了给定股票数量、面值和分红百分比时计算股息的公式或算法。开始和结束是入口和出口符号。输入股票数量、股票面值和分红百分比使用输入符号。计算股息和输出股息分别使用任务符号和输出符号:

在下一节中,我们将探讨伪代码,即算法的表示。

伪代码

伪代码是程序或算法的高级设计。顺序和选择是伪代码中使用的两种结构。与流程图相比,伪代码更容易可视化算法,同时伪代码可以轻松修改和更新。设计中的错误可以在伪代码的早期阶段被发现。这可以节省以后修复缺陷的成本。

举例来说,我们想要在一个长度为 n 的数组中找到 max 值。伪代码将如下所示:

maximum(arr) {
    n <- len(arr)
    max <- arr[0]
    for k <- 0,n do  {
        If  arr[k] > max {
            max <- arr[k]
        }
    }
    return max
}

现在我们已经知道了表示算法的不同方法,让我们看看在下一节中我们如何监控其复杂性和性能。

复杂度与性能分析

算法的效率是通过各种参数来衡量的,例如 CPU 时间、内存、磁盘和网络。复杂度是当输入参数的数量增加时算法如何扩展。性能是时间、空间、内存和其他参数的度量。算法通过其处理时间和资源消耗进行比较。复杂度衡量参数,并使用大 O 符号表示。

算法复杂度分析

算法的复杂度是通过算法的速度来衡量的。通常,算法的性能会根据处理器速度、磁盘速度、内存和其他硬件参数的不同而有所不同。因此,使用渐近复杂度来衡量算法的复杂度。算法是一系列步骤,通过不同的操作来完成任务。算法完成所需的时间基于所采取的步骤数量。

假设一个算法遍历一个大小为 10 的数组 m,并将元素更新为索引和 200 的和。计算时间将是 10t,其中 t 是将两个整数相加并将它们更新到数组中所需的时间。下一步将是遍历数组后打印它们。t 时间参数将随着所使用的计算机硬件的不同而变化。从渐近的角度来看,计算时间随着 10 的因子增长,如下面的代码所示:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
// main method
func main() {
 var m [10]int
 var k int
for k = 0; k < 10; k++ {
 m[k] = k + 200
fmt.Printf("Element[%d] = %d\n", k, m[k] )
 }
}

执行以下命令:

go run complexity.go

以下截图显示了输出:

让我们看看下一节中不同的复杂度类型。

大 O 符号

T(n)时间函数表示基于大 O 符号的算法复杂度。T(n) = O(n)表示算法具有线性时间复杂度。使用大 O 符号,常数时间、线性时间、对数时间、立方时间和二次时间复杂度是算法的不同复杂度类型。

线性时间,O(n),在如线性搜索、遍历和查找数组元素的最小和最大数量等场景中用作复杂度的度量。ArrayList 和队列是具有这些方法的数据结构。具有对数时间,O(log n)的算法是在树数据结构中的二分搜索。冒泡排序、选择排序和插入排序算法具有二次时间复杂度,O(n²)。大 Omega Ω和大 Theta Θ是表示特定算法下限和上限的符号。

最坏情况、最好情况、平均情况和摊销运行时间复杂度用于算法分析。摊销运行时间复杂度被称为 2^n。从渐近的角度来看,它将趋向于O(1)。

大 O 符号也用于确定算法消耗的空间量。这有助于我们找到相对于空间和时间的最优和最坏情况。

让我们看一下下一节中的线性复杂度。

线性复杂度

如果算法的处理时间或存储空间与要处理的输入元素数量成正比,则该算法为线性复杂度。在大 O 符号中,线性复杂度表示为O(n)。例如,Boyer-Moore 和 Ukkonen 字符串匹配算法具有线性复杂度。

线性复杂度,O(n),如下算法所示:


//main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
 "fmt"
)
// main method
func main() {
 var m [10]int
 var k int
for k = 0; k < 10; k++ {
 m[k] = k * 200
fmt.Printf("Element[%d] = %d\n", k, m[k] )
 }
}

运行以下命令:

go run linear_complexity.go

以下截图显示了输出:

图片

让我们看一下下一节中的二次复杂度。

二次复杂度

如果算法的处理时间与输入元素数量的平方成正比,则该算法为二次复杂度。在以下情况下,算法的复杂度为 1010 = 100。两个循环的最大值为10。n 个元素的乘法表的二次复杂度为O*(n²)。

二次复杂度,O(n²),在以下示例中展示:

//main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
    "fmt"
)
// main method
func main() {
    var k,l int
    for k = 1; k <= 10; k++ {
        fmt.Println(" Multiplication Table", k)
        for l=1; l <= 10; l++ {
            var x int = l *k
            fmt.Println(x)
        }
    }
}

运行以下命令:

go run quadratic_complexity.go

以下截图显示了输出:

图片

让我们看一下下一节中的立方复杂度。

立方复杂度

在立方复杂度的情况下,算法的处理时间与输入元素的立方成正比。以下算法的复杂度为 101010 = 1,000。三个循环的最大值为 10。矩阵更新的立方复杂度为O(n³)。

以下示例解释了立方复杂度 O(n³*):

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
// main method
func main() {
var k,l,m int
var arr[10][10][10] int
 for k = 0; k < 10; k++ {
for l=0; l < 10; l++ {
for m=0; m < 10; m++ {
arr[k][l][m] = 1
fmt.Println("Element value ",k,l,m," is", arr[k][l][m])
}
}
}
}

运行以下命令:

go run cubic_complexity.go

以下截图显示了输出:

图片

让我们看看下一节的对数复杂度。

对数复杂度

如果算法的处理时间与输入元素的对数成比例,则该算法具有对数复杂度。对数的底数通常是 2。以下树是一个具有LeftNodeRightNode的二元树。插入操作具有O(log n)复杂度,其中n是节点数。

对数复杂度如下所示:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
    "fmt"
)
// Tree struct
type Tree struct {
    LeftNode *Tree
    Value int
    RightNode *Tree
}

如以下代码所示,Tree类具有insert方法,该方法插入给定的整数元素m

// Tree insert method for inserting at m position
func (tree *Tree) insert( m int) {
 if tree != nil {
if tree.LeftNode == nil {
tree.LeftNode = &Tree{nil,m,nil}
 } else {
 if tree.RightNode == nil {
 tree.RightNode = &Tree{nil,m,nil}
 } else {
if tree.LeftNode != nil {
tree.LeftNode.insert(m)
} else {
tree.RightNode.insert(m)
}
}
}
} else {
tree = &Tree{nil,m,nil}
 }
}
//print method for printing a Tree
func print(tree *Tree) {
 if tree != nil {
fmt.Println(" Value",tree.Value)
 fmt.Printf("Tree Node Left")
 print(tree.LeftNode)
 fmt.Printf("Tree Node Right")
 print(tree.RightNode)
 } else {
 fmt.Printf("Nil\n")
 }
}

main方法在tree上调用insert方法以插入1357元素,如下代码所示:

// main method
func main() {
 var tree *Tree = &Tree{nil,1,nil}
 print(tree)
 tree.insert(3)
 print(tree)
 tree.insert(5)
 print(tree)
 tree.LeftNode.insert(7)
 print(tree)
}

运行以下命令:

go run tree.go

以下截图显示了输出:

图片

既然我们已经了解了算法的复杂性和分析其性能,那么让我们在下一节看看暴力算法。

暴力算法

暴力算法基于陈述和问题定义来解决问题。搜索和排序的暴力算法是顺序搜索和选择排序。穷举搜索是另一种暴力算法,其中解决方案是一组具有确定属性的候选解决方案。搜索发生的空间是一个状态和组合空间,它由排列、组合或子集组成。

暴力算法以其广泛的应用性和解决复杂问题的简单性而闻名。搜索、字符串匹配和矩阵乘法是一些它们被使用的场景。单个计算任务可以使用暴力算法解决。它们不提供有效的算法。这些算法速度慢,性能不佳。以下代码展示了暴力算法的表示:

//main package has examples shown
//in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
    "fmt"
)
//findElement method given array and k element
func findElement(arr[10] int, k int) bool {
    var i int
    for i=0; i< 10; i++ {
        if arr[i]==k {
            return true
        }
    }
    return false
}
// main method
func main() {
    var arr = [10]int{1,4,7,8,3,9,2,4,1,8}
    var check bool = findElement(arr,10)
    fmt.Println(check)
    var check2 bool = findElement(arr,9)
    fmt.Println(check2)
}

运行以下命令:

go run bruteforce.go

以下截图显示了输出:

图片

在介绍完暴力算法之后,我们将在下一节介绍分而治之算法。

分而治之算法

分而治之算法将复杂问题分解为更小的问题,并解决这些较小的问题。较小的问题将进一步分解,直到成为已知问题。方法是递归解决子问题并合并子问题的解决方案。

递归、快速排序、二分搜索、快速傅里叶变换和归并排序是分而治之算法的好例子。这些算法有效地使用了内存。在递归的情况下,性能有时是一个问题。在多处理器机器上,这些算法可以在将它们分解为子问题后在不同的处理器上执行。以下代码展示了分而治之算法:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
    "fmt"
)

如以下代码所示,斐波那契方法接受一个整数参数k,并返回k的斐波那契数。该方法使用递归来计算斐波那契数。递归算法通过将问题分解为k-1整数和k-2整数来应用:


// fibonacci method given k integer
func fibonacci(k int) int {
if k<=1{
 return 1
 }
 return fibonacci(k-1)+fibonacci(k-2)
}
// main method
func main() {
var m int = 5
for m=0; m < 8; m++ {
var fib = fibonacci(m)
fmt.Println(fib)
 }
}

运行以下命令:

go run divide.go

下面的截图显示了输出:

让我们看看下一节中回溯算法是什么。

回溯算法

回溯算法通过逐步构建解决方案来解决一个问题。评估多个选项,并通过递归选择算法的下一个解决方案组件。回溯可以是按时间顺序的类型,也可以根据问题遍历路径。根据你解决的问题,回溯可以是按时间顺序的类型,也可以遍历路径。

回溯是一种算法,它基于候选解决方案的可行性和有效性来找到候选解决方案并拒绝它。在查找无序表中的值等场景中,回溯非常有用。它比暴力搜索算法更快,后者在迭代中拒绝大量解决方案。回溯用于解决约束满足问题,如解析、规则引擎、背包问题和组合优化问题。

下面的例子是一个回溯算法。问题是识别一个包含 10 个元素的数组中,元素组合的和等于18findElementsWithSum方法递归地尝试找到组合。每当和超过k目标时,它就会回溯:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
//findElementsWithSum of k from arr of size
func findElementsWithSum(arr[10] int,combinations[19] int,size int, k int, addValue int, l int, m int) int {
var num int = 0
if addValue > k {
 return -1
 }
if addValue == k {
 num = num +1
 var p int =0
 for p=0; p < m; p++ {
  fmt.Printf("%d,",arr[combinations[p]])
  }
 fmt.Println(" ")
 }
var i int
for i=l; i< size; i++ {
//fmt.Println(" m", m)
combinations[m] = l
findElementsWithSum(arr,combinations,size,k,addValue+arr[i],l,m+1)
l = l+1
 }
 return num
}
// main method
func main() {
var arr = [10]int{1,4,7,8,3,9,2,4,1,8}
var addedSum int = 18
var combinations [19]int
findElementsWithSum(arr,combinations,10,addedSum,0,0,0)
//fmt.Println(check)//var check2 bool = findElement(arr,9)
//fmt.Println(check2)
}

运行以下命令:

go run backtracking.go

下面的截图显示了输出:

摘要

本章介绍了抽象数据类型的定义,将数据结构分为线性、非线性、同构、异构和动态类型。本章介绍了容器、列表、集合、映射、图、栈和队列等抽象数据类型。本章还涵盖了数据结构的性能分析和结构设计模式。

我们研究了数据结构的分类和结构设计模式。你可以通过计算复杂度和性能分析来使用诸如暴力搜索、分而治之、回溯等算法。算法的选择以及设计模式和数据结构的使用是关键要点。

在下一章中,我们将讨论 Go 中的数据结构。以下数据结构将被涵盖:

  • 数组

  • 切片

  • 二维切片

  • 映射

问题和练习

  1. 给出一个可以使用组合模式的例子。

  2. 对于一个包含 10 个元素的随机整数数组,识别最大值和最小值。计算算法的复杂度。

  3. 为了管理对象的状态,哪种结构模式是相关的?

  4. 一个窗口被子类化以添加滚动条,使其成为可滚动的窗口。在这种情况下应用了哪种模式?

  5. 找出二叉树搜索算法的复杂度。

  6. 在一个 3x3 矩阵中识别 2x2 的子矩阵。你所使用的算法的复杂度是多少?

  7. 用场景解释暴力搜索算法和回溯算法之间的区别。

  8. 规则引擎使用回溯来识别受变化影响的规则。请展示一个示例,说明回溯如何识别受影响的规则。

  9. 绘制一个流程图,用于计算成本价、售价和数量的利润亏损算法。

  10. 编写一个算法的伪代码,该算法比较字符串并识别字符串中的子字符串。

进一步阅读

如果你想了解更多关于四人帮设计模式、算法和数据结构的内容,以下书籍推荐:

  • 《设计模式》,作者:艾里克·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利斯塞斯

  • 《算法导论 第 3 版》,作者:托马斯·H·科门、查尔斯·E·莱伊森、罗纳德·L·里维斯和克利福德·斯坦

  • 《数据结构与算法:简单入门》,作者:鲁道夫·拉塞尔

第二章:Go 语言数据结构和算法入门

Go 编程语言已被开发者迅速采用,用于构建 Web 应用程序。凭借其令人印象深刻的性能和易于开发的特点,Go 拥有广泛的开放源代码框架支持,用于构建可扩展且高性能的 Web 服务和应用程序。迁移到 Golang 主要是由于其快速、轻量级和内置的并发特性。这也带来了学习与这种日益增长的语言相关的数据结构和算法的需求。

在数据结构中,单一类型的元素集合被称为数组切片与数组类似,但它们具有一些不寻常的特性。使用appendcopy方法扩大切片、分配切片的一部分、追加切片以及追加切片的一部分等切片操作将通过代码示例进行展示。数据库操作和 CRUD 网络表单是展示 Go 语言数据结构和算法的场景。

在本章中,我们将讨论以下 Go 语言特定的数据结构:

  • 数组

  • 切片

  • 二维切片

  • Maps

  • 数据库操作

  • 可变参数函数

  • CRUD 网络表单

技术要求

根据您的操作系统,在golang.org/doc/install安装 Go 版本 1.10。

本章的代码文件可以在以下 GitHub URL 找到:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter02

在本章中,数据库操作需要github.com/go-sql-driver/mysql包。此外,还需要从dev.mysql.com/downloads/mysql/安装 MySQL(4.1+)。

运行以下命令:

go get -u github.com/go-sql-driver/mysql

数组

数组是不同编程语言中最著名的数據結構。不同的數據類型可以作为数组的元素,例如intfloat32double等。以下代码片段展示了数组的初始化(arrays.go):

var arr = [5]int {1,2,4,5,6}

数组的尺寸可以通过len()函数找到。使用for循环可以访问数组中的所有元素,如下所示:

var i int
for i=0; i< len(arr); i++ {
    fmt.Println("printing elements ",arr[i]
}

在以下代码片段中,将详细解释range关键字。range关键字可以用来访问每个元素的索引和值:

var value int
for i, value = range arr{
    fmt.Println(" range ",value)
}

如果忽略索引,可以使用_空标识符。以下代码展示了如何使用_空标识符:

for _, value = range arr{
    fmt.Println("blank range",value)
}

运行以下命令:

go run arrays.go

以下截图显示了输出:

图片

Go 数组不是动态的,但有固定的大小。要添加比大小更多的元素,需要创建一个更大的数组,并将旧数组中的所有元素复制过来。数组通过复制数组作为值通过函数传递。传递大数组到函数可能会成为性能问题。

既然我们已经了解了数组是什么,那么让我们在下一节看看切片。

切片

Go 切片Go 数组的抽象。Go 数组允许存储多个相同类型的数据元素。Go 数组允许定义可以存储多个相同类型数据元素的变量,但在 Go 中没有提供任何内置方法来增加其大小。这个缺点由切片来弥补。Go 切片可以在容量达到其大小时追加元素。切片是动态的,并且可以将其当前容量加倍以添加更多元素。

让我们看看下一节中的 len 函数。

len 函数

len() 函数给出 slice 的当前长度,而 slice 的容量可以通过使用 cap() 函数获得。以下代码示例显示了基本的切片创建和追加切片 (basic_slice.go):

var slice = []int{1,3,5,6}
slice = append(slice, 8)
fmt.Println(“Capacity”, cap(slice))
fmt.Println(“Length”, len(slice))

运行以下命令以执行前面的代码:

go run basic_slice.go

以下截图显示了输出:

让我们看看下一节中的 slice 函数。

切片函数

切片是通过引用传递给函数的。大切片可以传递给函数而不影响性能。在代码中,以下是如何将切片作为引用传递给函数的示例 (slices.go):

//twiceValue method given slice of int type
func twiceValue(slice []int) {
      var i int
      var value int
for i, value = range  slice {
      slice[i] = 2*value
   }
    }
// main method
func main() {
    var slice = []int{1,3,5,6}
    twiceValue(slice)
    var i int
    for i=0; i< len(slice); i++ {
        fmt.Println(“new slice value”, slice[i])
}
}

运行以下命令:

go run slices.go

以下截图显示了输出:

既然我们已经知道了切片是什么,那么让我们继续到下一节学习二维切片。

二维切片

二维切片是二维数组的描述符。二维切片是一个数组中连续的部分,它存储在切片本身之外。它持有对底层数组的引用。二维切片将是一个数组的数组,而切片的容量可以通过创建一个新的切片并将初始切片的内容复制到新切片中来增加。这也被称为切片的切片。以下是一个二维数组的示例。创建了一个二维数组,并将数组元素初始化为值。

twodarray.go 是以下代码中展示的代码示例:

//main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
 "fmt"
)
// main method
func main() {
  var TwoDArray [8][8]int
   TwoDArray[3][6] = 18
  TwoDArray[7][4] = 3
   fmt.Println(TwoDArray)
}

运行以下命令:

go run twodarray.go

以下截图显示了输出:

对于动态分配,我们使用切片的切片。在以下代码中,切片的切片被解释为二维切片——twodslices.go

// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
 "fmt"
)
// main method
func main() {
   var rows int
   var cols int
   rows = 7
   cols = 9
   var twodslices = make([][]int, rows)
   var i int
   for i = range twodslices {
      twodslices[i] = make([]int,cols)
   }
    fmt.Println(twodslices)
}

运行以下命令:

go run twodslices.go

以下截图显示了输出:

在切片上使用的append方法用于向切片追加新元素。如果切片容量已达到底层数组的大小,则追加通过创建一个新的底层数组并添加新元素来增加大小。slic1arr从 0 开始到 3(不包括)的子切片,而slic2arr从 1(包括)到 5(不包括)的子切片。在以下代码片段中,append方法在slic2上调用以添加新的12元素(append_slice.go):

var arr = [] int{5,6,7,8,9}
var slic1 = arr[: 3]
fmt.Println("slice1",slic1)
var slic2 = arr[1:5]
fmt.Println("slice2",slic2)
var slic3 = append(slic2, 12)
fmt.Println("slice3",slic3)

运行以下命令:

go run append_slice.go

以下截图显示了输出:

图片

现在我们已经涵盖了二维切片的内容,让我们在下一节中看看映射。

映射

映射用于跟踪键类型,如整数、字符串、浮点数、双精度浮点数、指针、接口、结构体和数组。值可以是不同类型。在以下示例中,创建了一个具有整数键和字符串值的映射类型语言(maps.go):

var languages = map[int]string {
     3: “English”,
      4: “French”,
       5: “Spanish”
}

可以使用make方法创建映射,指定键类型和值类型。以下代码片段显示了具有整数键和字符串值的映射类型的产物:

var products = make(map[int]string)
products[1] = “chair”
products[2] = “table”

for循环用于遍历映射。语言映射的遍历方式如下:

var i int
var value string
for i, value = range languages {
   fmt.Println("language",i, “:",value)
}
fmt.Println("product with key 2",products[2])

使用产品映射检索值和删除切片操作如下所示:

fmt.Println(products[2])
delete(products,”chair”) 
fmt.Println("products",products)

运行以下命令:

go run maps.go

以下截图显示了输出:

图片

现在我们已经涵盖了映射,让我们继续到数据库操作。

数据库操作

在本节中,我们将通过适当的示例查看一些数据库操作。

让我们从下一节的GetCustomer方法开始。

GetCustomer方法

GetCustomer方法从数据库中检索Customer数据。首先,以下示例显示了create数据库操作。Customer是具有CustomeridCustomerNameSSN属性的表。GetConnection方法返回数据库连接,用于查询数据库。查询然后返回数据库表中的行。以下代码详细解释了数据库操作(database_operations.go):

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt,database/sql, net/http, text/template package
import (
    "fmt"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

// Customer Class
type Customer struct {
    CustomerId int
    CustomerName string
    SSN string
}
// GetConnection method which returns sql.DB
func GetConnection() (database *sql.DB) {
    databaseDriver := "mysql"
    databaseUser := "newuser"
    databasePass := "newuser"
    databaseName := "crm"
    database, error := sql.Open(databaseDriver, databaseUser+":"+databasePass+"@/"+databaseName)
    if error != nil {
        panic(error.Error())
    }
    return database
}
// GetCustomers method returns Customer Array
func GetCustomers() []Customer {
    var database *sql.DB
    database = GetConnection()

    var error error
    var rows *sql.Rows
    rows, error = database.Query("SELECT * FROM Customer ORDER BY Customerid DESC")
    if error != nil {
        panic(error.Error())
    }
    var customer Customer
    customer = Customer{}

    var customers []Customer
    customers= []Customer{}
    for rows.Next() {
        var customerId int
        var customerName string
        var ssn string
        error = rows.Scan(&customerId, &customerName, &ssn)
        if error != nil {
            panic(error.Error())
        }
        customer.CustomerId = customerId
        customer.CustomerName = customerName
        customer.SSN = ssn
        customers = append(customers, customer)
    }

    defer database.Close()

    return customers
}

//main method
func main() {

     var customers []Customer
    customers = GetCustomers()
    fmt.Println("Customers",customers)

}

运行以下命令:

go run database_operations.go

以下截图显示了输出:

图片

让我们看一下下一节中的InsertCustomer方法。

InsertCustomer方法

INSERT操作如下。InsertCustomer方法接受Customer参数并创建一个用于INSERT语句的预处理语句。该语句用于将客户行插入到表中,如下面的代码片段所示:

// InsertCustomer method with parameter customer
func InsertCustomer(customer Customer) {
     var database *sql.DB
     database= GetConnection()

      var error error
      var insert *sql.Stmt
      insert,error = database.Prepare("INSERT INTO CUSTOMER(CustomerName,SSN) VALUES(?,?)")
          if error != nil {
              panic(error.Error())
          }
          insert.Exec(customer.CustomerName,customer.SSN)

      defer database.Close()

}

让我们在下一节中看看可变参数函数。

可变参数函数

一个函数,我们传递无限数量的参数,而不是一次传递一个,称为可变参数函数。最终参数的类型前面有一个省略号 (...), 在声明可变参数函数时;这表明该函数可以带有任何数量的此类参数。

可变参数函数可以用可变数量的参数调用。fmt.Println 是一个常见的可变参数函数,如下所示:

//main method
func main() {
     var customers []Customer
    customers = GetCustomers()
    fmt.Println("Before Insert",customers)
    var customer Customer
    customer.CustomerName = "Arnie Smith"
    customer.SSN = "2386343"
    InsertCustomer(customer)
    customers = GetCustomers()
    fmt.Println("After Insert",customers)
    }

运行以下命令:

go run database_operations.go

以下截图显示了输出:

图片

让我们在下一节开始 update 操作。

更新操作

update 操作如下。UpdateCustomer 方法接收 Customer 参数并创建一个用于 UPDATE 语句的预处理语句。该语句用于在表中更新一行客户数据:

// Update Customer method with parameter customer
func UpdateCustomer(customer Customer){
     var database *sql.DB
     database= GetConnection()
     var error error
      var update *sql.Stmt
      update,error = database.Prepare("UPDATE CUSTOMER SET CustomerName=?, SSN=? WHERE CustomerId=?")
          if error != nil {
           panic(error.Error())
          } 
      update.Exec(customer.CustomerName,customer.SSN,customer.CustomerId)
defer database.Close()
}
// main method
func main() {
    var customers []Customer
    customers = GetCustomers()
   fmt.Println("Before Update",customers)
   var customer Customer
    customer.CustomerName = "George Thompson"
    customer.SSN = "23233432"
    customer.CustomerId = 5
    UpdateCustomer(customer)
    customers = GetCustomers()
    fmt.Println("After Update",customers)
}

运行以下命令:

go run database_operations.go

以下截图显示了输出:

图片

让我们看一下下一节中的 delete 操作。

删除操作

delete 操作如下。DeleteCustomer 方法接收 Customer 参数并创建一个用于 DELETE 语句的预处理语句。该语句用于在表中删除一行客户数据:

// Delete Customer method with parameter customer
func deleteCustomer(customer Customer){
     var database *sql.DB
     database= GetConnection()
     var error error
      var delete *sql.Stmt
      delete,error = database.Prepare("DELETE FROM Customer WHERE Customerid=?")
          if error != nil {
             panic(error.Error())
         }
          delete.Exec(customer.CustomerId)
      defer database.Close()
}
// main method
func main() {
     var customers []Customer
    customers = GetCustomers()
    fmt.Println("Before Delete",customers)
  var customer Customer
  customer.CustomerName = "George Thompson"
  customer.SSN = "23233432"
  customer.CustomerId = 5
    deleteCustomer(customer)
    customers = GetCustomers()
    fmt.Println("After Delete",customers)
}

运行以下命令:

go run database_operations.go

以下截图显示了输出:

图片

现在我们已经完成了可变参数函数的讨论,让我们继续在下一节查看 CRUD 网页表单。

CRUD 网页表单

在本节中,我们将通过基本示例解释网页表单,展示如何执行各种操作。

要使用 Go 的 net/http 包启动一个基本的 HTML 页面,以下是一个网页表单示例 (webforms.go)。在 main.html 中有一个欢迎问候语:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt, database/sql, net/http, text/template package
import (
    "net/http"
    "text/template"
    "log")
// Home method renders the main.html
func Home(writer http.ResponseWriter, reader *http.Request) {
    var template_html *template.Template
    template_html = template.Must(template.ParseFiles("main.html"))
    template_html.Execute(writer,nil)
}
// main method
func main() {
    log.Println("Server started on: http://localhost:8000")
    http.HandleFunc("/", Home)
    http.ListenAndServe(":8000", nil)
}

main.html 的代码如下**:

<html>
    <body>
        <p> Welcome to Web Forms</p>
    </body>
</html>

运行以下命令:

go run webforms.go

以下截图显示了输出:

图片

以下截图显示了网页浏览器的输出:

图片

以网页表单为例构建的 CRM 应用程序用于演示 CRUD 操作。我们可以使用上一节中构建的数据库操作。以下代码展示了 crm 数据库操作。crm 数据库操作包括 CRUD 方法,如 CREATEREADUPDATEDELETE 客户操作。GetConnection 方法检索用于执行数据库操作的数据库连接 (crm_database_operations.go):

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt,database/sql, net/http, text/template package
import (
   "database/sql"
    _ "github.com/go-sql-driver/mysql"
)
// Customer Class
type Customer struct {
    CustomerId    int
    CustomerName  string
    SSN string
}
//  GetConnection method  which returns sql.DB
func GetConnection() (database *sql.DB) {
    databaseDriver := "mysql"
    databaseUser := "newuser"
    databasePass := "newuser"
    databaseName := “crm"
    database, error := sql.Open(databaseDriver, databaseUser+”:"+databasePass+"@/"+databaseName)
    if error != nil {
        panic(error.Error())
    }
    return database
}

如以下代码所示,GetCustomerById 方法接收 customerId 参数以在客户数据库中查找。GetCustomerById 方法返回客户对象:

//GetCustomerById with parameter customerId returns Customer
func GetCustomerById(customerId int) Customer {
  var database *sql.DB
  database = GetConnection()
  var error error
  var rows *sql.Rows
  rows, error = database.Query("SELECT * FROM Customer WHERE CustomerId=?",customerId)
  if error != nil {
      panic(error.Error())
  }
  var customer Customer
  customer = Customer{}
    for rows.Next() {
        var customerId int
        var customerName string
        var SSN  string
        error = rows.Scan(&customerId, &customerName, &SSN)
        if error != nil {
           panic(error.Error())
        }
        customer.CustomerId = customerId
        customer.CustomerName = customerName
       customer.SSN = SSN
    }

现在我们已经涵盖了 CRUD 网页表单,让我们在下一节继续探讨 deferpanic

延迟和 panic 语句

defer 语句将函数的执行推迟到周围函数返回。panic 函数停止当前流程和控制。在 panic 调用之后,延迟的函数会正常执行。在以下代码示例中,即使调用了 panic 调用,defer 调用也会执行:

    defer database.Close()
    return customer
}
// GetCustomers method returns Customer Array
func GetCustomers() []Customer {
    var database *sql.DB
    database = GetConnection()
    var error error
    var rows *sql.Rows
    rows, error = database.Query("SELECT * FROM Customer ORDER BY Customerid DESC")
    if error != nil {
       panic(error.Error())
    }
   var customer Customer
    customer = Customer{}
    var customers []Customer
    customers= []Customer{}
   for rows.Next() {
       var customerId int
        var customerName string
       var ssn string
        error = rows.Scan(&customerId, &customerName, &ssn)
        if error != nil {
            panic(error.Error())
        }
        customer.CustomerId = customerId
        customer.CustomerName = customerName
        customer.SSN = ssn
        customers = append(customers, customer)
    }
   defer database.Close()
    return customers
}

让我们看一下以下章节中的 InsertCustomerUpdateCustomerDeleteCustomer 方法。

InsertCustomer 方法

在以下代码中,InsertCustomer 方法将 customer 作为参数来执行插入到 CUSTOMER 表的 SQL 语句:

// InsertCustomer method with parameter customer
func InsertCustomer(customer Customer) {
     var database *sql.DB
     database= GetConnection()
     var error error
      var insert *sql.Stmt
     insert,error = database.Prepare("INSERT INTO CUSTOMER(CustomerName,SSN) VALUES(?,?)")
          if error != nil {
              panic(error.Error())
         }
     insert.Exec(customer.CustomerName,customer.SSN)
      defer database.Close()
}

UpdateCustomer 方法

UpdateCustomer 方法通过传递 CustomerNameSSNcustomer 对象来准备 UPDATE 语句;如下面的代码所示:

// Update Customer method with parameter customer
func UpdateCustomer(customer Customer) {
     var database *sql.DB
     database= GetConnection()
     var error error
      var update *sql.Stmt
      update,error = database.Prepare("UPDATE CUSTOMER SET CustomerName=?, SSN=? WHERE CustomerId=?")
          if error != nil {
              panic(error.Error())
          }
          update.Exec(customer.CustomerName,customer.SSN,customer.CustomerId)
     defer database.Close()
}

DeleteCustomer 方法

DeleteCustomer 方法通过执行 DELETE 语句来删除传递的客户:

// Delete Customer method with parameter customer
func DeleteCustomer(customer Customer) {
     var database *sql.DB
     database= GetConnection()
      var error error
      var delete *sql.Stmt
      delete,error = database.Prepare("DELETE FROM Customer WHERE Customerid=?")
          if error != nil {
              panic(error.Error())
          }
          delete.Exec(customer.CustomerId)
     defer database.Close()
}

让我们看一下下一节中的 CRM 网络应用程序。

CRM 网络应用程序

如下所示,CRM 网络应用程序处理了各种网络路径。CRM 应用程序代码如下。Home 函数使用 writer 参数和客户数组执行 Home 模板(crm_app.go):


//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt,database/sql, net/http, text/template package
import (
    "fmt"
    "net/http"
    "text/template"
    "log"
)

var template_html = template.Must(template.ParseGlob("templates/*"))

// Home - execute Template
func Home(writer http.ResponseWriter, request *http.Request) {
    var customers []Customer
    customers = GetCustomers()
    log.Println(customers)
    template_html.ExecuteTemplate(writer,"Home",customers)

}

让我们看一下以下章节中的 CreateInsertAlterUpdateDelete 函数,以及 main 方法。

Create 函数

如以下代码所示,Create 函数接受 writerrequest 参数来渲染 Create 模板:

// Create - execute Template
func Create(writer http.ResponseWriter, request *http.Request) {

    template_html.ExecuteTemplate(writer,"Create",nil)
}

Insert 函数

Insert 函数调用 GetCustomers 方法获取 customers 数组,并通过调用 ExecuteTemplate 方法,使用 writercustomers 数组作为参数来渲染 Home 模板。这如下面的代码所示:

// Insert - execute template
func Insert(writer http.ResponseWriter, request *http.Request) {

    var customer Customer
    customer.CustomerName = request.FormValue("customername")
    customer.SSN = request.FormValue("ssn")
    InsertCustomer(customer)
    var customers []Customer
    customers = GetCustomers()
    template_html.ExecuteTemplate(writer,"Home",customers)

}

Alter 函数

以下代码展示了 Alter 函数如何通过使用 writercustomers 数组作为参数调用 ExecuteTemplate 方法来渲染 Home 模板:

// Alter - execute template
func Alter(writer http.ResponseWriter, request *http.Request) {

    var customer Customer
    var customerId int
    var customerIdStr string
    customerIdStr = request.FormValue("id")
    fmt.Sscanf(customerIdStr, "%d", &customerId)
    customer.CustomerId = customerId
    customer.CustomerName = request.FormValue("customername")
    customer.SSN = request.FormValue("ssn")
    UpdateCustomer(customer)
    var customers []Customer
    customers = GetCustomers()
    template_html.ExecuteTemplate(writer,"Home",customers)

}

Update 函数

Update 函数使用 writer 和通过 id 查找的 customer 调用 ExecuteTemplate 方法。ExecuteTemplate 方法渲染 UPDATE 模板:

// Update - execute template
func Update(writer http.ResponseWriter, request *http.Request) {

  var customerId int
  var customerIdStr string
  customerIdStr = request.FormValue("id")
  fmt.Sscanf(customerIdStr, "%d", &customerId)
  var customer Customer
  customer = GetCustomerById(customerId)

    template_html.ExecuteTemplate(writer,"Update",customer)

}

Delete 函数

Delete 方法在通过 GetCustomerById 方法找到客户后渲染 Home 模板。View 方法在通过调用 GetCustomerById 方法找到客户后渲染 View 模板:

// Delete - execute Template
func Delete(writer http.ResponseWriter, request *http.Request) {
  var customerId int
  var customerIdStr string
  customerIdStr = request.FormValue("id")
  fmt.Sscanf(customerIdStr, "%d", &customerId)
  var customer Customer
  customer = GetCustomerById(customerId)
   DeleteCustomer(customer)
   var customers []Customer
   customers = GetCustomers()
  template_html.ExecuteTemplate(writer,"Home",customers)

}
// View - execute Template
func View(writer http.ResponseWriter, request *http.Request) {
    var customerId int
    var customerIdStr string
    customerIdStr = request.FormValue("id")
    fmt.Sscanf(customerIdStr, "%d", &customerId)
    var customer Customer
    customer = GetCustomerById(customerId)
    fmt.Println(customer)
    var customers []Customer
    customers= []Customer{customer}
    customers.append(customer)
    template_html.ExecuteTemplate(writer,"View",customers)

}

main 方法

main 方法处理 HomeAlterCreateUpdateViewInsertDelete 函数,并使用不同的别名进行查找,并适当地渲染模板。HttpServer 监听端口 8000 并等待模板别名的调用:

// main method
func main() {
    log.Println("Server started on: http://localhost:8000")
    http.HandleFunc("/", Home)
    http.HandleFunc("/alter", Alter)
    http.HandleFunc("/create", Create)
    http.HandleFunc("/update", Update)
    http.HandleFunc("/view", View)
    http.HandleFunc("/insert", Insert)
    http.HandleFunc("/delete", Delete)
    http.ListenAndServe(":8000", nil)
}

让我们看一下以下章节中的 HeaderFooterMenuCreateUpdateView 模板。

Header 模板

Header 模板在以下代码片段中定义了 HTML 的 headbody 部分,如下所示。网页的标题标签设置为 CRM,网页内容为 Customer Management – CRMHeader.tmpl):

{{ define "Header" }}
<!DOCTYPE html>
<html>
    <head>
        <title>CRM</title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <h1>Customer Management – CRM</h1>   
{{ end }}

Footer 模板

Footer 模板定义了 HTML 和 BODY 的关闭标签。以下代码片段展示了 Footer 模板(Footer.tmpl):

{{ define "Footer" }}
    </body>
  </html>
{{ end }}

菜单模板

Menu 模板定义了 HomeCreate Customer 的链接,以下代码展示了该模板(Menu.tmpl):

{{ define "Menu" }}
<a href="/">Home</a> |<a href="/create">Create Customer</a>
{{ end }}

创建模板

Create 模板由 HeaderMenuFooter 模板组成。创建客户字段的表单位于 create 模板中。此表单提交到网络路径—/insert,以下代码片段展示了该模板(Create.tmpl):

{{ define "Create" }}
  {{ template "Header" }}
  {{ template "Menu"  }}
  <br>
    <h1>Create Customer</h1>
  <br>
  <br>
  <form method="post" action="/insert">
    Customer Name: <input type="text" name="customername" placeholder="customername" autofocus/>
    <br>
    <br>
    SSN: <input type="text" name="ssn" placeholder="ssn"/>
    <br>
    <br>
    <input type="submit" value="Create Customer"/>
   </form>
{{ template "Footer" }}
{{ end }}

更新模板

Update 模板由 HeaderMenuFooter 模板组成,如下所示。更新客户字段的表单位于 Update 模板中。此表单提交到网络路径 /alterUpdate.tmpl):

{{ define "Update" }}
  {{ template "Header" }}
    {{ template "Menu" }}
<br>
<h1>Update Customer</h1>
    <br>
    <br>
  <form method="post" action="/alter">
    <input type="hidden" name="id" value="{{ .CustomerId }}" />
    Customer Name: <input type="text" name="customername" placeholder="customername" value="{{ .CustomerName }}" autofocus>
    <br>
    <br>
    SSN: <input type="text" name="ssn" value="{{ .SSN }}" placeholder="ssn"/>
    <br>
    <br>
    <input type="submit" value="Update Customer"/>
   </form>
{{ template "Footer" }}
{{ end }}

View 模板

View 模板由 HeaderMenuFooter 模板组成。查看客户字段的表单位于 View 模板中,以下代码展示了该模板(View.tmpl):

{{ define "View" }}
  {{ template "Header" }}
  {{ template "Menu"  }}
    <br>
       <h1>View Customer</h1>
      <br>
      <br>
<table border="1">
<tr>
<td>CustomerId</td>
<td>CustomerName</td>
<td>SSN</td>
<td>Update</td>
<td>Delete</td>
</tr>
{{ if . }}
       {{ range . }}
<tr>
<td>{{ .CustomerId }}</td>
<td>{{ .CustomerName }}</td>
<td>{{ .SSN }}</td>
<td><a href="/delete?id={{.CustomerId}}" onclick="return confirm('Are you sure you want to delete?');">Delete</a> </td>
<td><a href="/update?id={{.CustomerId}}">Update</a> </td>
</tr>
{{ end }}
     {{ end }}
</table>
{{ template "Footer" }}
{{ end }}

执行以下命令:

go run crm_app.go crm_database_operations.go

以下截图显示了输出:

图片

以下截图显示了网络浏览器的输出:

图片

摘要

本章介绍了数据库操作和网络表单。现在,你将能够构建可以存储数据库数据的网络应用程序。本章通过代码示例涵盖了数组、切片、二维切片和地图。本章使用代码片段解释了数组方法,如 len、使用 for 迭代数组和使用 range。在 切片 部分讨论了二维数组和切片的切片。

使用各种场景解释了地图,例如添加键和值,以及检索和删除值。本章还讨论了不同类型的地图,如字符串和整数。此外,在 数据库操作CRUD 网络表单 部分演示了可变参数函数、延迟函数调用和恐慌与恢复操作(数据库操作CRUD 网络表单 部分)。

CRM 应用程序被构建为一个网络应用程序,数据持久化在 MySQL 数据库中。代码片段展示了添加、删除、更新和检索数据的数据库操作。此外,使用模板的网络表单展示了创建、更新、删除和查看客户数据。本章 技术要求 部分提供了 MySQL 驱动及其安装细节。本章通过执行细节演示了如何使用 Go 创建网络应用程序。

下一章将涉及与线性数据结构相关的主题,如列表、集合、元组和栈。

问题

  1. 获取数组大小的方法叫什么?

  2. 你如何找到切片的容量?

  3. 你如何初始化字符串类型的二维切片?

  4. 你如何向切片中添加一个元素?

  5. 使用代码,你能演示如何创建一个键为字符串、值为字符串的映射吗?在代码中初始化映射的键和值,在循环中迭代它们,并在代码中打印键和值。

  6. 你如何在映射中删除一个值?

  7. 获取数据库连接需要哪些参数?

  8. 哪个sql.Rows类方法使得在表中读取实体的属性成为可能?

  9. 当数据库连接关闭时,defer做了什么?

  10. 哪个方法允许sql.DB类创建一个预处理语句?

进一步阅读

想要了解更多关于数组、映射和切片的信息,以下链接推荐阅读:

  • 《学习 Go 数据结构和算法 [视频]》,作者 Gustavo Chaín

  • 精通 Go,作者 Mihalis Tsoukalos

第二部分:使用 Go 语言的基本数据结构和算法

我们将讨论数据结构,包括线性、非线性、同构、异构和动态类型,以及经典算法。本部分涵盖了不同类型的列表、树、数组、集合、字典、元组、堆、队列和栈,以及排序、递归、搜索和哈希算法。

本部分包含以下章节:

  • 第三章,线性 数据结构

  • 第四章,非线性 数据结构

  • 第五章,同构 数据结构

  • 第六章,异构 数据结构

  • 第七章,动态 数据结构

  • 第八章,经典算法

第三章:线性数据结构

诸如 Facebook、Twitter 和 Google 等各种应用程序都使用列表和线性数据结构。正如我们之前所讨论的,数据结构允许我们以顺序和组织的方式组织大量数据,从而在处理此类数据时减少时间和精力。列表、栈、集合和元组是一些常用的线性数据结构。

在本章中,我们将通过给出涉及这些数据结构的各种过程示例来讨论这些数据结构。我们将讨论与这些数据结构相关的各种操作,例如插入、删除、更新、遍历(列表)、反转以及与各种代码示例的合并。

在本章中,我们将介绍以下线性数据结构:

  • 列表

  • 集合

  • 元组

技术要求

根据您的操作系统,在golang.org/doc/install安装 Go 版本 1.10。

本章的代码文件可以在以下 GitHub URL 找到:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter03

列表

列表是一组有序元素集合,用于存储项目列表。与数组列表不同,这些列表可以动态地扩展和收缩。

列表还可以用作其他数据结构的基础,例如栈和队列。列表可以用来存储用户列表、汽车零部件、成分、待办事项以及各种其他元素。列表是最常用的线性数据结构。这些是在 lisp 编程语言中引入的。在本章中,我们将使用 Go 语言介绍链表和双链表。

在下一节中,我们将讨论与链表和双链表相关的添加、更新、删除和查找操作。

链表

LinkedList是一系列具有属性和指向序列中下一个节点的引用的节点。它是一种用于存储数据的线性数据结构。该数据结构允许从任何节点添加和删除与另一个节点相邻的组件。它们不是在内存中连续存储的,这使得它们与数组不同

在以下部分,我们将查看链表中的结构和方法。

节点类

Node类有一个名为property的整型变量。该类还有一个名为nextNode的变量,它是一个指向序列中下一个节点的指针。链表将包含具有整型属性的节点集合,如下所示:

//Node class
type Node struct {
    property int
    nextNode *Node
}

链表类

LinkedList类具有headNode指针作为其属性。通过从headNode遍历到nextNode,您可以遍历链表,如下面的代码所示:

// LinkedList class
type LinkedList struct {
    headNode *Node
}

在以下章节中讨论了LinkedList类的不同方法,例如AddtoHeadIterateListLastNodeAddtoEndNodeWithValueAddAftermain方法。

添加到头部的方法

AddToHead方法将节点添加到链表的开始。LinkedList类的AddToHead方法有一个整数属性参数。该属性用于初始化节点。实例化一个新节点,并将其属性设置为传递的property参数。nextNode指向linkedList的当前headNode,并将headNode设置为创建的新节点的指针,如下面的代码所示:

//AddToHead method of LinkedList class
func (linkedList *LinkedList) AddToHead(property int) {
    var node = Node{}
    node.property = property
    if node.nextNode != nil {
        node.nextNode = linkedList.headNode
    }
    linkedList.headNode = &node
}

当具有1属性的节点添加到头部时,将1属性添加到linkedList的头部将headNode设置为具有值1currentNode,如下面的截图所示:

使用main方法执行此命令。在这里,我们创建了一个LinkedList类的实例,并将整数属性13添加到该实例的头部。在添加元素后,打印出链表的headNode属性,如下所示:

// main method
func main() {
    var linkedList LinkedList
    linkedList = LinkedList{}
    linkedList.AddToHead(1)
    linkedList.AddToHead(3)
    fmt.Println(linkedList.headNode.property)
}

执行以下命令以运行linked_list.go文件:

go run linked_list.go

执行前面的命令后,我们得到以下输出:

让我们看一下下一节中的IterateList方法。

迭代列表方法

LinkedList类的IterateList方法从headNode属性开始迭代并打印当前头节点的属性。迭代发生在头节点移动到headNode属性的nextNode,直到当前节点不再等于nil。以下代码显示了LinkedList类的IterateList方法:

//IterateList method iterates over LinkedList
func (linkedList *LinkedList) IterateList() {
    var node *Node
    for node = linkedList.headNode; node != nil; node = node.nextNode {
        fmt.Println(node.property)
    }
}

最后一个节点方法

LinkedList类的LastNode方法返回列表末尾的节点。遍历列表以检查从headNodenextNode开始的nextNode是否为nil,如下所示:

//LastNode method returns the last Node

func (linkedList *LinkedList) LastNode() *Node{
 var node *Node
 var lastNode *Node
 for node = linkedList.headNode; node != nil; node = node.nextNode {
 if node.nextNode ==nil {
 lastNode = node
 }
 }
 return lastNode
}

添加到末尾方法

AddToEnd方法将节点添加到列表的末尾。在以下代码中,找到了当前的lastNode,并将其nextNode属性设置为添加的节点:

//AddToEnd method adds the node with property to the end

func (linkedList *LinkedList) AddToEnd(property int) {
 var node = &Node{}
 node.property = property
 node.nextNode = nil
 var lastNode *Node
 lastNode = linkedList.LastNode()
 if lastNode != nil {
 lastNode.nextNode = node
 }
}

在下面的截图中,当具有属性值5的节点添加到末尾时调用了AddToEnd方法。通过此方法添加属性创建了一个值为5的节点。列表的最后一个节点具有属性值5lastNodenextNode属性为nillastNodenextNode被设置为具有值5的节点:

让我们看一下下一节中的NodeWithValue方法。

节点值方法

在下面的代码片段中,LinkedList类的NodeWithValue方法返回具有property值的节点。遍历列表并检查property值是否等于参数property

//NodeWithValue method returns Node given parameter property

func (linkedList *LinkedList) NodeWithValue(property int) *Node{
 var node *Node
 var nodeWith *Node
 for node = linkedList.headNode; node != nil; node = node.nextNode {
 if node.property == property {
 nodeWith = node
 break;
 }
 }
 return nodeWith
}

添加到后面方法

AddAfter方法在特定节点之后添加节点。LinkedListAddAfter方法有nodePropertyproperty参数。使用NodeWithValue方法检索具有nodeProperty值的节点。创建一个具有property的节点并将其添加到NodeWith节点之后,如下所示:

//AddAfter method adds a node with nodeProperty after node with property

func (linkedList *LinkedList) AddAfter(nodeProperty int,property int) {
 var node = &Node{}
 node.property = property
 node.nextNode = nil
 var nodeWith *Node
 nodeWith = linkedList.NodeWithValue(nodeProperty)
 if nodeWith != nil {
 node.nextNode = nodeWith.nextNode
 nodeWith.nextNode = node
 }
}

当调用AddAfter方法并在具有属性值7的节点之后添加具有值1的节点时,你将得到以下输出。具有属性值1的节点的nextNode属性为 nil。具有属性值1的节点的nextNode属性被设置为具有值5的节点:

图片

让我们在下一节中看看main方法。

main方法

main方法添加了具有整数属性135的节点,如下面的代码所示。在具有整数属性1的节点之后添加了一个具有整数属性7的节点。在linkedList实例上调用IterateList方法,如下所示:

// main method
func main() {
    var linkedList LinkedList
    linkedList = LinkedList{}
    linkedList.AddToHead(1)
    linkedList.AddToHead(3)
    linkedList.AddToEnd(5)
    linkedList.AddAfter(1,7)
    linkedList.IterateList()
}

main方法将13添加到链表的头部。5被添加到末尾。7被添加到1之后。链表将是3175

运行以下命令以执行linked_list.go文件:

go run linked_list.go

执行前面的命令后,我们得到以下输出:

图片

让我们在下一节中看看双向链表。

双向链表

在双向链表中,所有节点都有一个指向它们在列表中连接的节点的指针,位于它们的两侧。这意味着每个节点连接到两个节点,我们可以通过前向遍历到下一个节点或通过后向遍历到前一个节点。双向链表允许插入、删除以及显然的遍历操作。节点类定义在以下代码示例中:

// Node class
type Node struct {
    property int
    nextNode *Node
    previousNode *Node
}

以下各节解释了双向链表方法,例如NodeBetweenValuesAddToHeadAddAfterAddToEndmain方法。

NodeBetweenValues方法

LinkedList类的NodeBetweenValues方法返回具有位于firstPropertysecondProperty值之间的属性的节点。该方法遍历列表以确定firstPropertysecondProperty整数属性是否在连续的节点上匹配,如下所示:

//NodeBetweenValues method of LinkedList
func (linkedList *LinkedList) NodeBetweenValues(firstProperty int,secondProperty int) *Node{
    var node *Node
    var nodeWith *Node
    for node = linkedList.headNode; node != nil; node = node.nextNode {
        if node.previousNode != nil && node.nextNode != nil {
            if node.previousNode.property == firstProperty && node.nextNode.property ==    
            secondProperty{
               nodeWith = node
               break;
            }
        }
    }
    return nodeWith
}

在调用NodeBetweenValues方法并使用15作为参数后,以下截图显示了示例输出。lastNodenextNode被设置为具有值5的节点。具有属性值7的节点位于具有属性值15的节点之间:

图片

让我们在下一节中看看AddToHead方法。

AddToHead方法

LinkedList 类的 AddToHead 方法将当前链表 headNodepreviousNode 属性设置为具有属性值的添加的节点。具有属性值的节点将被设置为以下代码中 LinkedList 方法的 headNode

//AddToHead method of LinkedList
func (linkedList *LinkedList) AddToHead(property int) {
 var node = &Node{}
 node.property = property
 node.nextNode = nil
 if linkedList.headNode != nil {
 node.nextNode = linkedList.headNode
 linkedList.headNode.previousNode = node
 }
 linkedList.headNode = node
}

调用 AddToHead 方法并使用属性 3 后的示例输出如下。创建了一个属性值为 3 的节点。列表的 headNode 属性的属性值为 1。当前属性值为 3 的节点有一个 nextNode 属性为 nil。当前节点的 nextNode 属性被设置为属性值为 1 的 headNodeheadNodepreviousNode 属性被设置为当前节点:

图片

让我们看一下下一节中的 AddAfter 方法。

AddAfter 方法

AddAfter 方法将节点添加到双链表中特定节点之后。双 LinkedList 类的 AddAfter 方法搜索值等于 nodeProperty 的节点。找到的节点被设置为具有属性值的节点的 previousNode。添加的节点的 nextNode 将是 nodeWith 属性的 nextNode。添加的节点的 previousNode 将是值等于 nodeProperty 的找到的节点。nodeWith 节点将被更新为当前节点。以下代码显示了 AddAfter 方法:

//AddAfter method of LinkedList
func (linkedList *LinkedList) AddAfter(nodeProperty int,property int) {
 var node = &Node{}
 node.property = property
 node.nextNode = nil
 var nodeWith *Node
 nodeWith = linkedList.NodeWithValue(nodeProperty)
 if nodeWith != nil {

 node.nextNode = nodeWith.nextNode
 node.previousNode = nodeWith
 nodeWith.nextNode = node
 }
}

调用 AddAfter 方法并使用属性 7 后的示例输出如下。创建了一个属性值为 7 的节点。创建的节点的 nextNode 属性为 nil。创建的节点的 nextNode 属性被设置为属性值为 1 的 headNodeheadNodepreviousNode 属性被设置为当前节点:

图片

让我们看一下下一节中的 AddToEnd 方法。

AddToEnd 方法

AddToEnd 方法将节点添加到双链表的末尾。LinkedList 类的 AddToEnd 方法创建一个属性设置为整型参数 property 的节点。该方法将添加的节点的 previousNode 属性设置为当前 lastNode 属性,如下所示。当前 lastNode 属性的 nextNode 设置为在末尾添加具有属性值的节点,如下所示:

//AddToEnd method of LinkedList
func (linkedList *LinkedList) AddToEnd(property int) {
 var node = &Node{}
 node.property = property
 node.nextNode = nil
 var lastNode *Node
 lastNode = linkedList.LastNode()
 if lastNode != nil {

 lastNode.nextNode = node
 node.previousNode = lastNode
 }
}

调用 AddToEnd 方法并使用属性 5 后的示例输出如下。创建了一个属性值为 5 的节点。列表的 lastNode 的属性值为 1。lastNodenextNode 属性为 nillastNodenextNode 被设置为属性值为 5 的节点。创建的节点的 previousNode 被设置为属性值为 1 的节点:

图片

让我们看一下下一节中的 main 方法。

主方法

在以下代码片段中,main 方法调用 NodeBetweenValues 属性,使用 firstPropertysecondProperty。打印值 15 之间的节点属性:

// main method
func main() {
 var linkedList LinkedList
 linkedList = LinkedList{}
 linkedList.AddToHead(1)
 linkedList.AddToHead(3) linkedList.AddToEnd(5)
 linkedList.AddAfter(1,7)
 fmt.Println(linkedList.headNode.property)
 var node *Node
 node = linkedList.NodeBetweenValues(1,5)
 fmt.Println(node.property)
}

main 方法创建了一个链表。节点被添加到头部和尾部。在值 15 之间的节点被搜索,并打印其属性。

运行以下命令以执行 doubly_linked_list.go 文件:

go run doubly_linked_list.go

执行前面的命令后,我们得到以下输出:

图片

下一节将讨论集合,它们是线性数据结构。

集合

集合是一个线性数据结构,它包含一组不重复的值。集合可以存储无特定顺序的唯一值。在现实世界中,集合可以用来收集博客文章和聊天参与者的所有标签。数据可以是布尔型、整数、浮点数、字符和其他类型。静态集合只允许查询操作,这意味着与查询元素相关的操作。动态可变集合允许插入和删除元素。可以在集合上定义代数运算,如并集、交集、差集和子集。以下示例显示了具有 map 整数键和 bool 值的 Set 整数:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
)
//Set class
type Set struct {
 integerMap map[int]bool
}
//create the map of integer and bool
func (set *Set) New(){
 set.integerMap = make(map[int]bool)
}

在以下各节中讨论了 AddElementDeleteElementContainsElementIntersectUnionmain 方法。

AddElement 方法

AddElement 方法将元素添加到集合中。在以下代码片段中,如果元素不在 Set 中,Set 类的 AddElement 方法将元素添加到 integerMap 中。integerMap 元素具有整数键和 bool 值,如下所示:

// adds the element to the set
func (set *Set) AddElement(element int){
 if !set.ContainsElement(element) {
  set.integerMap[element] = true
 }
}

使用参数 2 调用 AddElement 方法后的示例输出如下。检查是否存在值为 2 的元素。如果没有元素,则将映射设置为 true,键为 2:

图片

让我们看看下一节中的 DeleteElement 方法。

DeleteElement 方法

DeleteElement 方法使用 delete 方法从 integerMap 中删除元素。此方法从集合的 integerMap 中删除元素,如下所示:

//deletes the element from the set
func (set *Set) DeleteElement(element int) {
    delete(set.integerMap,element)
}

ContainsElement 方法

Set 类的 ContainsElement 方法检查元素是否存在于 integerMap 中。使用以下代码示例中的键整数元素查找 integerMap 元素:


//checks if element is in the set
func (set *Set) ContainsElement(element int) bool{
 var exists bool
 _, exists = set.integerMap[element]
 return exists
}

main 方法 – 包含元素

在以下代码片段中,main 方法创建 Set,调用 New 方法,并添加元素 12。检查元素 1 是否存在于集合中:

// main method
func main() {
    var set *Set
    set = &Set{}
    set.New()
    set.AddElement(1)
    set.AddElement(2)
    fmt.Println(set)
    fmt.Println(set.ContainsElement(1))
}

运行以下命令以执行 set.go 文件:

go run set.go

执行前面的命令后,我们得到以下输出:

图片

让我们看看下一节中的 InterSect 方法。

InterSect 方法

在以下代码中,Set类上的InterSect方法返回一个由setanotherSet的交集组成的intersectionSet。通过integerMap遍历set类,并与另一个Set进行比较,以查看是否存在任何元素:

//Intersect method returns the set which intersects with anotherSet

func (set *Set) Intersect(anotherSet *Set) *Set{
 var intersectSet = & Set{}
 intersectSet.New()
 var value int
 for(value,_ = range set.integerMap){
   if anotherSet.ContainsElement(value) {
    intersectSet.AddElement(value)
   }
 }
 return intersectSet 
}

调用带有另一个Set参数的intersect方法后的示例输出如下。创建一个新的intersectSet。遍历当前set,检查每个值是否在另一个set中。如果值在另一个set中,则将其添加到set的交集:

图片

让我们在下一节看看Union方法。

Union方法

Set类上的Union方法返回一个由setanotherSet的并集组成的unionSet。通过integerMap键遍历两个集合,并将来自集合的元素更新到并集,如下所示:

//Union method returns the set which is union of the set with anotherSet

func (set *Set) Union(anotherSet *Set) *Set{
 var unionSet = & Set{}
 unionSet.New()
 var value int
 for(value,_ = range set.integerMap){
   unionSet.AddElement(value)
 }

 for(value,_ = range anotherSet.integerMap){
   unionSet.AddElement(value)
 }

 return unionSet 
}

调用带有anotherSet参数的union方法后的示例输出如下。创建一个新的unionSet。遍历当前集合和另一个集合的值。将每个值添加到并集:

图片

让我们在下一节看看main方法。

main方法 - 交集和并集

在以下代码片段中,main方法在集合类上调用intersectunion,并传递anotherSet参数。交集和并集集合按以下方式打印:

// main method
func main() {
 var set *Set
 set = &Set{}
 set.New()
 set.AddElement(1)
 set.AddElement(2)
 fmt.Println("initial set", set)
 fmt.Println(set.ContainsElement(1))
 var anotherSet *Set
 anotherSet = &Set{}
 anotherSet.New()
 anotherSet.AddElement(2)
 anotherSet.AddElement(4)
 anotherSet.AddElement(5) fmt.Println(set.Intersect(anotherSet))
 fmt.Println(set.Union(anotherSet))
}

main方法接受两个集合,并找到这两个集合的交集和并集。

执行以下命令以执行set.go文件:

go run set.go

执行前面的命令后,我们得到以下输出:

图片

下一节将讨论元组,它们是有序对象的有限序列。

元组

元组是有序对象的有限序列。它们可以包含其他数据类型的混合,并用于将相关数据分组到数据结构中。在关系型数据库中,元组是表中的一行。与列表相比,元组具有固定的大小,并且运行速度更快。关系型数据库中有限个元组的集合被称为关系实例。元组可以在单个语句中赋值,这对于交换值很有用。列表通常包含相同数据类型的值,而元组包含不同的数据。例如,我们可以在元组中存储用户的姓名、年龄和最喜欢的颜色。元组在第一章,《数据结构和算法》中有所介绍。以下示例展示了函数调用中的多值表达式(tuples.go):

//main package has examples shown
 // in Hands-On Data Structures and algorithms with Go book
 package main
 // importing fmt package
 import (
 "fmt"
 )
 //h function which returns the product of parameters x and y
 func h(x int, y int) int {
 return x*y
 }
 // g function which returns x and y parameters after modification
 func g(l int, m int) (x int, y int) {
 x=2*l
 y=4*m
 return
 }
 // main method
 func main() {
 fmt.Println(h(g()))
 }

main函数以g函数作为参数调用h函数。g函数返回xy整数组成的元组。

执行以下命令以执行tuples.go文件:

go run tuples.go

执行前面的命令后,我们得到以下输出:

图片

下一节将讨论队列,它们是线性数据结构。

队列

队列由按特定顺序或基于优先级处理的元素组成。以下代码显示了基于优先级的订单队列,其结构为堆。可以在队列上执行入队、出队和查看等操作。队列是一种线性数据结构,是一种顺序集合。元素添加到集合的末尾,从集合的开始处移除。队列通常用于存储需要执行的任务,或需要由服务器处理的传入 HTTP 请求。在现实生活中,处理实时系统中的中断、呼叫处理和 CPU 任务调度是使用队列的好例子。

以下代码显示了订单队列以及如何定义Queue类型:

// Queue—Array of Orders Type
type Queue []*Order

// Order class
type Order struct {
    priority int
    quantity int
    product string
    customerName string
}

本章的以下部分讨论队列的NewAddmain方法。

新建方法

Order类上的New方法将priorityquantityproduct参数的属性分配给namecustomerName。该方法初始化订单的属性如下:

// New method initializes with Order with priority, quantity, product, customerName
func (order *Order) New(priority int, quantity int, product string, customerName string ){
 order.priority = priority
 order.quantity = quantity
 order.product = product
 order.customerName = customerName
 }

添加方法

在以下代码片段中,Queue类上的Add方法接受order参数并根据优先级将其添加到Queue中。基于此,通过比较order参数与priority参数找到order参数的位置:

//Add method adds the order to the queue
func (queue *Queue) Add(order *Order){
 if len(*queue) == 0 {
 *queue = append(*queue,order)
 } else{
 var appended bool
 appended = false
 var i int
 var addedOrder *Order
 for i, addedOrder = range *queue {
 if order.priority > addedOrder.priority {
 *queue = append((*queue)[:i], append(Queue{order}, (*queue)[i:]...)...)
 appended = true
 break
 }
 }
 if !appended {
 *queue = append(*queue, order)
 }
 }
}

在使用order参数调用add方法后的示例输出如下。检查订单是否存在于队列中。然后将订单追加到队列中:

让我们看看下一节中的Main方法。

主方法 – 队列

main方法创建了两个订单,并将订单的优先级设置为21。在以下代码中,队列将首先处理优先级值较高的订单:


// main method
func main() {
 var queue Queue
 queue = make(Queue,0)
 var order1 *Order = &Order{}
 var priority1 int = 2
 var quantity1 int = 20
 var product1 string = "Computer"
 var customerName1 string = "Greg White"
 order1.New(priority1,quantity1,product1, customerName1)
 var order2 *Order = &Order{}
 var priority2 int = 1
 var quantity2 int = 10
 var product2 string = "Monitor"
 var customerName2 string = "John Smith"
 order2.New(priority2,quantity2,product2, customerName2)
 queue.Add(order1)

 queue.Add(order2)
var i int
for i=0; i< len(queue); i++ {
fmt.Println(queue[i])
}
}

运行以下命令以执行queue.go文件:

go run queue.go

执行前面的命令后,我们得到以下输出:

让我们看看下一节中的同步队列

同步队列

同步队列由需要按特定顺序处理的元素组成。乘客队列和票务处理队列是同步队列的类型,如下所示:


//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
 "time"
 "math/rand"
)
// constants
const (
 messagePassStart = iota
 messageTicketStart
 messagePassEnd
 messageTicketEnd
)
//Queue class
type Queue struct {
 waitPass int
 waitTicket int
 playPass bool
 playTicket bool
 queuePass chan int
 queueTicket chan int
 message chan int
}

我们将在以下部分讨论同步队列的不同方法。

新建方法

Queue上的New方法使用nil值初始化messagequeuePassqueueTicketmake方法创建一个带有chan整数参数的Queue,如下所示:

// New method initializes queue
func (queue *Queue) New() {
 queue.message = make(chan int)
 queue.queuePass= make(chan int)
 queue.queueTicket= make(chan int)
 }

在以下代码示例中,Go例程根据消息类型和相应的队列来选择消息:

go func() {
 var message int
 for {
 select {
 case message = <-queue.message:
 switch message {
 case messagePassStart:
 queue.waitPass++
 case messagePassEnd:
 queue.playPass = false
 case messageTicketStart:
 queue.waitTicket++
 case messageTicketEnd:
 queue.playTicket = false
 }
 if queue.waitPass > 0 && queue.waitTicket > 0 && !queue.playPass && !queue.playTicket {
 queue.playPass = true
 queue.playTicket = true
 queue.waitTicket--
 queue.waitPass--
 queue.queuePass <- 1
 queue.queueTicket <- 1
 }
 }
 }
 }()
}

开始发票方法

StartTicketIssue 方法启动对排队乘客的票务发放。Queue 上的 StartTicketIssue 方法向消息队列发送 messageTicketStart 消息,queueTicket 接收该消息。通过向队列发送消息开始票务发放,如下所示:

// StartTicketIssue starts the ticket issue
func (Queue *Queue) StartTicketIssue() {
 Queue.message <- messageTicketStart
 <-Queue.queueTicket
}

The EndTicketIssue method

EndTicketIssue 方法完成对排队乘客的票务发放。在以下代码中,Queue 上的 EndTicketIssue 方法向消息队列发送 messageTicketEnd 消息。通过发送以下消息结束票务发放:

// EndTicketIssue ends the ticket issue
func (Queue *Queue) EndTicketIssue() {
 Queue.message <- messageTicketEnd
}

The ticketIssue method

ticketIssue 方法开始并结束对乘客的票务发放。ticketIssue 方法在调用 Sleep 10 秒和 2 秒后调用 StartTicketIssueEndTicketIssue 方法。票务在票务处理完毕后发放,如下所示代码:

//ticketIssue starts and ends the ticket issue
func ticketIssue(Queue *Queue) {
 for {
 // Sleep up to 10 seconds.
 time.Sleep(time.Duration(rand.Intn(10000)) * time.Millisecond)
 Queue.StartTicketIssue()
 fmt.Println("Ticket Issue starts")
 // Sleep up to 2 seconds.
 time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
 fmt.Println("Ticket Issue ends")
 Queue.EndTicketIssue()
 }
}

The StartPass method

StartPass 方法启动乘客队列向售票柜台移动。Queue 上的 StartPass 方法向消息队列发送 messagePassStart 消息,queuePass 接收该消息。乘客按照以下方式被移动到队列中:

//StartPass ends the Pass Queue
func (Queue *Queue) StartPass() {
    Queue.message <- messagePassStart
    <-Queue.queuePass
}

The EndPass method

EndPass 方法停止乘客队列向售票柜台移动。在以下代码中,Queue 上的 EndPass 方法向消息队列发送 messagePassEnd 消息。乘客被移动到柜台进行票务处理,然后乘客离开队列:

//EndPass ends the Pass Queue
func (Queue *Queue) EndPass() {
    Queue.message <- messagePassEnd
}

The passenger method

passenger 方法启动并结束乘客向队列的移动。passenger 方法调用 StartPass 方法,并在调用 sleep 10 秒和 2 秒后结束 EndPass 方法。乘客进入队列并到达售票柜台,如下所示代码:

//passenger method starts and ends the pass Queue
func passenger(Queue *Queue) {
 //fmt.Println("starting the passenger Queue")
 for {
 // fmt.Println("starting the processing")
 // Sleep up to 10 seconds.
 time.Sleep(time.Duration(rand.Intn(10000)) * time.Millisecond)
 Queue.StartPass()
 fmt.Println(" Passenger starts")
 // Sleep up to 2 seconds.
 time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
 fmt.Println( " Passenger ends")
 Queue.EndPass()
 }
}

The main method

main 方法在创建队列后调用 passengerticketIssue 方法。乘客进入队列,在处理队列的柜台发放票务,如下所示代码:

// main method
func main() {
 var Queue *Queue = & Queue{}
 //fmt.Println(Queue)
 Queue.New()
 fmt.Println(Queue)
 var i int
 for i = 0; i < 10; i++ {
 // fmt.Println(i, "passenger in the Queue")
 go passenger(Queue)
 }
 //close(Queue.queuePass)
 var j int
 for j = 0; j < 5; j++ {
 // fmt.Println(i, "ticket issued in the Queue")
 go ticketIssue(Queue)
 }
 select {}
}

运行以下命令以执行 sync_queue.go 文件:

go run sync_queue.go

执行上述命令后,我们得到以下输出:

下一个部分将讨论 Stacks,它们是线性数据结构。

Stacks

栈是一种后进先出结构,其中项目从顶部添加。栈用于解析器中解决迷宫算法。Pushpoptopget size 是允许在栈数据结构上执行的典型操作。语法解析、回溯和编译时内存管理是一些可以使用栈的真实场景。以下是一个栈实现的示例(stack.go):

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing fmt package
import (
 "fmt"
 "strconv"
)
//Element class
type Element struct {
 elementValue int
}
// String method on Element class
func (element *Element) String() string {
 return strconv.Itoa(element.elementValue)
}

Element 类有一个 elementValue 属性。String 方法返回元素的 elementValue

在以下章节中介绍了栈方法,如 NewPushPopmain

The New method

Stack类的New方法创建了一个动态元素数组。Stack类具有元素的计数和数组指针。以下是与Stack类定义和New方法相关的代码片段:

// NewStack returns a new stack.
func (stack *Stack) New() {
 stack.elements = make(*Element[] elements,0)
}
// Stack is a basic LIFO stack that resizes as needed.
type Stack struct {
 elements []*Element
 elementCount int
}

推入方法

Push方法将节点添加到stack类的顶部。在下面的代码示例中,Stack类的Push方法将元素添加到元素数组中,并增加Count元素,而append方法将元素添加到stack类的元素中:

// Push adds a node to the stack.
func (stack *Stack) Push(element *Element) {
 stack.elements = append(stack.elements[:stack.elementCount], element)
 stack.elementCount = stack.elementCount + 1
}

调用带有参数元素的push方法后的示例输出如下。将值 7 的元素推入栈中。推入栈之前的元素计数为 2,推入栈后这个数字变为 3:

图片

让我们看看下一节的Pop方法。

弹出方法

Stack实现的Pop方法从元素数组中移除最后一个元素并返回该元素,如下面的代码所示。len方法返回元素数组的长度:

// Pop removes and returns a node from the stack in last to first order.
func (stack *Stack) Pop() *Element {
 if stack.elementCount == 0 {
 return nil
 }
 var length int = len(stack.elements)
 var element *Element = stack.elements[length -1]
 //stack.elementCount = stack.elementCount - 1
 if length > 1 {
 stack.elements = stack.elements[:length-1]
 } else {
 stack.elements = stack.elements[0:]
 }
 stack.elementCount = len(stack.elements)
 return element
}

调用Pop方法后的示例输出如下。将元素值 5 传递并添加到Pop方法中。调用Pop方法之前的元素计数为 2。调用Pop方法后的元素计数为 1:

图片

让我们看看下一节的main方法。

主方法

在以下代码部分,main方法创建了一个stack,调用New方法,并在初始化后推入元素。打印出弹出的元素值和顺序:

// main method
func main() {
 var stack *Stack = & Stack{}
 stack.New()
 var element1 *Element = &Element{3}
 var element2 *Element = &Element{5}
 var element3 *Element = &Element{7}
 var element4 *Element = &Element{9}
 stack.Push(element1)
 stack.Push(element2)
 stack.Push(element3)
 stack.Push(element4)
 fmt.Println(stack.Pop(), stack.Pop(), stack.Pop(), stack.Pop())
}

执行以下命令以运行stack.go文件:

go run stack.go

执行前面的命令后,我们得到以下输出:

图片

摘要

本章涵盖了LinkedList、双LinkedListTuplesSetsQueuesStacks的定义。本章还介绍了LinkedList方法——AddToHeadAddToEndLastNodeiterateList。此外,优先队列被建模为待处理订单的堆,同步队列被展示为乘客和票务处理队列,元组在函数返回多值表达式的上下文中进行了解释。Stacknewpushpopstring方法通过代码示例进行了说明。

在下一章中,我们将涵盖TreesTablesContainersHash函数等领域。

问题

  1. 你可以在哪里使用双链表?请提供一个示例。

  2. 哪个链表方法可以用来打印节点值?

  3. 哪个队列是用 Go 语言中的通道展示的?

  4. 编写一个返回多个值的方法。可以使用什么数据结构来返回多个值?

  5. 能设置有重复元素的吗?

  6. 编写一个代码示例,展示两个集合的并集和交集。

  7. 在链表中,使用哪种方法来查找两个值之间的节点?

  8. 我们有一些不重复且唯一的元素。表示这些集合的正确数据结构是什么?

  9. 在 Go 语言中,如何生成介于 3 和 5 之间的随机整数?

  10. 如何在 Set 中检查是否存在值为 5 的元素?

进一步阅读

要了解更多关于 LinkedListsSetsTuplesStacks 的信息,请参考以下资料:

  • 设计模式》,作者 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides

  • 算法导论 第 3 版》,作者 Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest 和 Clifford Stein

  • 数据结构与算法:简单入门》,作者 Rudolph Russell

第四章:非线性数据结构

非线性数据结构在密码学和其他领域中使用。非线性数据结构是一种元素连接到许多元素的排列。这些结构使用内存快速且高效。添加新元素不需要连续的空闲内存。

在添加新元素之前,数据结构的长度并不重要。非线性数据结构有多个级别,而线性数据结构只有一个级别。非线性数据结构中的元素值没有组织。非线性数据结构中的数据元素不能一步迭代。这些数据结构的实现复杂。

本章解释了二叉搜索树、堆和符号表等树类型。

本章涵盖了以下非线性数据结构:

  • 表格

  • 容器

  • 哈希函数

技术要求

golang.org/doc/install为您的操作系统安装 Go 版本 1.10。

本章代码的 GitHub URL 如下:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter04

树是一种非线性数据结构。树用于搜索和其他用例。二叉树的节点最多有两个子节点。二叉搜索树由节点组成,其中左节点的属性值小于右节点的属性值。根节点位于树的零级。每个子节点可以是叶子节点。

在我们讨论对数复杂度时,介绍了第一章“数据结构和算法”中的树和二叉树。让我们在下一节中更详细地看看它们。

二叉搜索树

二叉搜索树是一种允许快速查找、添加和删除元素的数据结构。它以排序顺序存储键以实现更快的查找。这种数据结构是由 P. F. Windley、A. D. Booth、A. J. T. Colin 和 T. N. Hibbard 发明的。平均而言,二叉搜索树的平均空间使用量为O(n),而插入、搜索和删除操作的平均时间为O(log n)。二叉搜索树由具有属性或属性的节点组成:

  • 一个key整数

  • 一个value整数

  • TreeNodeleftNoderightNode实例

它们可以用以下代码表示:

// TreeNode class
type TreeNode struct {
 key int
 value int
 leftNode *TreeNode 
 rightNode *TreeNode 
}

在下一节中,将讨论BinarySearchTree类的实现。对于本节,请参阅binary_search_tree.go文件。

二叉搜索树类

在以下代码片段中,BinarySearchTree类包含一个rootNode,它是TreeNode类型,以及一个sync.RWMutex类型的锁。二叉搜索树通过访问rootNode的左右节点从rootNode遍历:

// BinarySearchTree class
type BinarySearchTree struct {
 rootNode *TreeNode
 lock sync.RWMutex
}

既然我们已经知道了什么是 BinarySearchTree,那么让我们在下一节中看看它的不同方法。

InsertElement 方法

InsertElement 方法在二叉搜索树中插入具有给定键和值的元素。首先锁定树的 lock() 实例,并在插入元素之前延迟调用 unlock() 方法。通过传递 rootNode 和要创建的具有键和值的节点调用 InsertTreeNode 方法,如下所示:

// InsertElement method
func (tree *BinarySearchTree) InsertElement(key int, value int) {
 tree.lock.Lock()
 defer tree.lock.Unlock()
 var treeNode *TreeNode
 treeNode= &TreeNode{key, value, nil, nil}
 if tree.rootNode == nil {
 tree.rootNode = treeNode
 } else {
 insertTreeNode(tree.rootNode, treeNode)
 }
}

插入键和值为 3 的元素的示例输出如下。insert 元素方法调用 insertTreeNode,传递具有键 8rootNode 和具有键 3 的新 treeNode

图片

insertTreeNode 方法

insertTreenode 方法在二叉搜索树中插入新的 TreeNode。在以下代码中,insertTreeNode 方法接受 rootNodenewTreeNode 作为参数,两者都是 TreeNode 类型。注意,newTreeNode 通过比较键值被插入到二叉搜索树的正确位置:

// insertTreeNode function
func insertTreeNode(rootNode *TreeNode, newTreeNode *TreeNode) {
 if newTreeNode.key < rootNode.key {
 if rootNode.leftNode == nil {
 rootNode.leftNode = newTreeNode
 } else {
 insertTreeNode(rootNode.leftNode, newTreeNode)
 }
 } else {
 if rootNode.rightNode == nil{ 
 rootNode.rightNode = newTreeNode
 } else {
 insertTreeNode(rootNode.rightNode, newTreeNode)
 }
 }
}

inOrderTraverse 方法

inOrderTraverse 方法按顺序访问所有节点。首先在树 lock 实例上调用 RLock() 方法。在调用 inOrderTraverseTree 方法之前,在树 lock 实例上延迟调用 RUnLock() 方法,如下面的代码片段所示:

// InOrderTraverseTree method
func (tree *BinarySearchTree) InOrderTraverseTree(function func(int)) {
 tree.lock.RLock()
 defer tree.lock.RUnlock()
 inOrderTraverseTree(tree.rootNode, function)
}

inOrderTraverseTree 方法

inOrderTraverseTree 方法遍历左子树、根节点和右子树。inOrderTraverseTree 方法接受 TreeNode 类型的 treeNodefunction 作为参数。inOrderTraverseTree 方法在 leftNoderightNode 上调用,并传递 function 作为参数。functiontreeNode.value 作为参数传递,如下面的代码片段所示:

//  inOrderTraverseTree method
func inOrderTraverseTree(treeNode *TreeNode, function func(int)) {
 if treeNode != nil {
 inOrderTraverseTree(treeNode.leftNode, function)
 function(treeNode.value)
 inOrderTraverseTree(treeNode.rightNode, function)
 }
}

PreOrderTraverseTree 方法

PreOrderTraverseTree 方法以先序遍历的方式访问所有 tree 节点。首先锁定 treelock 实例,并在调用 preOrderTraverseTree 之前延迟调用 Unlock 方法。在以下代码片段中,preOrderTraverseTree 方法以 rootNodefunction 作为参数传递:

// PreOrderTraverseTree method
func (tree *BinarySearchTree) PreOrderTraverseTree(function func(int)) {
 tree.lock.Lock()
 defer tree.lock.Unlock()
 preOrderTraverseTree(tree.rootNode, function)
}

preOrderTraverseTree 方法

preOrderTraverseTree 方法以 TreeNode 类型的 treeNodefunction 作为参数传递。通过传递 leftNoderightNode 作为参数调用 preOrderTraverseTree 方法。functiontreeNode.value 作为参数调用,如下所示:

//  preOrderTraverseTree method
func preOrderTraverseTree(treeNode *TreeNode, function func(int)) {
 if treeNode != nil {
 function(treeNode.value)
 preOrderTraverseTree(treeNode.leftNode, function)
 preOrderTraverseTree(treeNode.rightNode, function)
 }
}

PostOrderTraverseTree 方法

PostOrderTraverseTree 方法以后序(左、右、当前节点)遍历节点。在以下代码片段中,BinarySearchTree 类的 PostOrderTraverseTree 方法以后序遍历的方式访问所有节点。将 function 方法作为参数传递给方法。首先锁定 tree.lock 实例,并在调用 postOrderTraverseTree 方法之前在树 lock 实例上延迟调用 Unlock 方法:

// PostOrderTraverseTree method
func (tree *BinarySearchTree) PostOrderTraverseTree(function func(int)) {
 tree.lock.Lock()
 defer tree.lock.Unlock()
 postOrderTraverseTree(tree.rootNode, function)
}

后序遍历树方法

postOrderTraverseTree 方法接受 TreeNode 类型的 treeNodefunction 作为参数。通过传递 leftNoderightNode 以及 function 作为参数来调用 postOrderTraverseTree 方法。在以下代码片段中,function 使用 treeNode.value 作为参数被调用:

//  postOrderTraverseTree method
func postOrderTraverseTree(treeNode *TreeNode, function func(int)) {
 if treeNode != nil {
 postOrderTraverseTree(treeNode.leftNode, function)
 postOrderTraverseTree(treeNode.rightNode, function)
 function(treeNode.value)
 }
}

最小节点方法

MinNode 在二叉搜索树中找到具有最小值的节点。在下面的代码片段中,首先调用树 lock 实例的 RLock 方法,然后延迟执行树 lock 实例上的 RUnlock 方法。MinNode 方法通过从 rootNode 开始遍历并检查 leftNode 的值是否为 nil 来返回具有最低值的元素:

// MinNode method
func (tree *BinarySearchTree) MinNode() *int {
 tree.lock.RLock()
 defer tree.lock.RUnlock()
 var treeNode *TreeNode
 treeNode = tree.rootNode
 if treeNode == nil {
 //nil instead of 0
 return (*int)(nil)
 }
 for {
 if treeNode.leftNode == nil {
 return &treeNode.value
 }
 treeNode = treeNode.leftNode
 }
}

最大节点方法

MaxNode 在二叉搜索树中找到具有最大属性的节点。首先调用树 lock 实例的 RLock 方法,然后延迟执行树 lock 实例上的 RUnlock 方法。MaxNode 方法在从 rootNode 遍历并找到一个具有 nil 值的 rightNode 后返回具有最高值的元素。这如下面的代码所示:

// MaxNode method
func (tree *BinarySearchTree) MaxNode() *int {
 tree.lock.RLock()
 defer tree.lock.RUnlock()
 var treeNode *TreeNode
 treeNode = tree.rootNode
 if treeNode == nil {
 //nil instead of 0
 return (*int)(nil)
 }
 for {
 if treeNode.rightNode == nil {
 return &treeNode.value
 }
 treeNode = treeNode.rightNode
 }
}

搜索节点方法

SearchNode 方法在二叉搜索树中搜索指定的节点。首先,调用树锁实例的 RLock 方法。然后,将树 lock 实例上的 RUnlock 方法延迟执行。BinarySearchTree 类的 SearchNode 方法使用 rootNodekey 整数值作为参数调用 searchNode 方法,如下所示:

// SearchNode method
func (tree *BinarySearchTree) SearchNode(key int) bool {
 tree.lock.RLock()
 defer tree.lock.RUnlock()
 return searchNode(tree.rootNode, key)
}

搜索节点方法

在以下代码中,searchNode 方法接受 TreeNode 类型的指针 treeNodekey 整数值作为参数。在检查是否存在与 key 值相同的 treeNode 后,该方法返回 truefalse

//  searchNode method
func searchNode(treeNode *TreeNode, key int) bool {
 if treeNode == nil {
 return false
 }
 if key < treeNode.key {
 return searchNode(treeNode.leftNode, key)
 }
 if key > treeNode.key {
 return searchNode(treeNode.rightNode, key)
 }
 return true
}

移除节点方法

BinarySearchTree 类的 RemoveNode 方法移除传入的 key 值对应的元素。该方法将 key 整数值作为参数。首先在树的 lock 实例上调用 Lock() 方法。然后延迟执行树 lock 实例的 Unlock() 方法,并使用 rootNodekey 值作为参数调用 removeNode,如下所示:

// RemoveNode method
func (tree *BinarySearchTree) RemoveNode(key int) {
 tree.lock.Lock()
 defer tree.lock.Unlock()
 removeNode(tree.rootNode, key)
}

移除节点方法

removeNode 方法接受 TreeNode 类型的 treeNodekey 整数值作为参数。在下面的代码片段中,该方法递归地搜索 treeNodeleftNode 实例和 rightNodekey 值,如果它与参数值匹配:

// removeNode method
func removeNode(treeNode *TreeNode, key int) *TreeNode {
 if treeNode == nil {
 return nil
 }
 if key < treeNode.key {
 treeNode.leftNode = removeNode(treeNode.leftNode, key)
 return treeNode
 }
 if key > treeNode.key {
 treeNode.rightNode = removeNode(treeNode.rightNode, key)
 return treeNode
 }
 // key == node.key
 if treeNode.leftNode == nil && treeNode.rightNode == nil {
 treeNode = nil
 return nil
 }
 if treeNode.leftNode == nil {
 treeNode = treeNode.rightNode
 return treeNode
 }
 if treeNode.rightNode == nil {
 treeNode = treeNode.leftNode
 return treeNode
 }
 var leftmostrightNode *TreeNode
 leftmostrightNode = treeNode.rightNode
 for {
 //find smallest value on the right side
 if leftmostrightNode != nil && leftmostrightNode.leftNode != nil {
 leftmostrightNode = leftmostrightNode.leftNode
 } else {
 break
 }
 }
 treeNode.key, treeNode.value = leftmostrightNode.key, leftmostrightNode.value
 treeNode.rightNode = removeNode(treeNode.rightNode, treeNode.key)
 return treeNode
}

字符串方法

String 方法将 tree 转换为字符串格式。首先在树的 lock 实例上调用 Lock() 方法。然后延迟执行树 lock 实例的 Unlock() 方法。String 方法打印 tree 的可视表示:

// String method
func (tree *BinarySearchTree) String() {
 tree.lock.Lock()
 defer tree.lock.Unlock()
 fmt.Println("------------------------------------------------")
 stringify(tree.rootNode, 0)
 fmt.Println("------------------------------------------------")
}

字符串化方法

在以下代码片段中,stringify 方法接受 TreeNode 类型的 treeNode 实例和 level(一个整数)作为参数。该方法根据级别递归打印树:

// stringify method
func stringify(treeNode *TreeNode, level int) {
 if treeNode != nil {
 format := ""
 for i := 0; i < level; i++ {
 format += " "
 }
 format += "--- "
 level++
 stringify(treeNode.leftNode, level)
 fmt.Printf(format+"%d\n", treeNode.key)
 stringify(treeNode.rightNode, level)
 }
}

主方法

在以下代码中,main 方法创建了一个二叉搜索树,并将元素 831016 插入其中。通过调用 String 方法打印 tree

// main method
func main() {
 var tree *BinarySearchTree = &BinarySearchTree{}
 tree.InsertElement(8,8)
 tree.InsertElement(3,3)
 tree.InsertElement(10,10)
 tree.InsertElement(1,1)
 tree.InsertElement(6,6)
 tree.String()
}

运行以下命令以执行 binary_search_tree.go 文件:

go run binary_search_tree.go

输出如下:

![

下一节讨论 AVL 树的实现。

Adelson, Velski, and Landis (AVL) 树

Adelson, Velski, and Landis 首创了 AVL 树数据结构,因此以他们的名字命名。它由调整高度的二叉搜索树组成。平衡因子是通过找到左右子树高度的差来获得的。平衡是通过旋转技术完成的。如果平衡因子大于一,则旋转将节点移至左子树或右子树的相反方向。搜索、添加和删除操作按 O(log n) 的顺序处理。

以下章节讨论了 KeyValue 接口定义和 TreeNode 类。对于本节,请参阅 avl_tree.go 文件。

KeyValue 接口

KeyValue 接口有 LessThanEqualTo 方法。LessThanEqualTo 方法接受 KeyValue 作为参数,并在检查小于或等于条件后返回一个布尔值。如下代码所示:

// KeyValue type
type KeyValue interface {
  LessThan(KeyValue) bool
  EqualTo(KeyValue) bool
}

TreeNode 类

TreeNode 类具有 KeyValueBalanceValueLinkedNodes 作为属性。AVL 树是一个由 TreeNode 类型节点组成的树,如下所示:

// TreeNode class
type TreeNode struct {
 KeyValue     KeyValue
 BalanceValue int
 LinkedNodes [2]*TreeNode
}

现在,让我们看看 TreeNode 类的不同方法。

相反的方法

opposite 方法接受一个节点值,并返回相反节点的值。在以下代码片段中,opposite 方法接受 nodeValue 整数作为参数,并返回相反节点的值:

//opposite method
func opposite(nodeValue int) int {
 return 1 - nodeValue
}

singleRotation 方法

singleRotation 方法旋转与指定子树相反的节点。如下代码片段所示,singleRotation 函数旋转与左子树或右子树相反的节点。该方法接受 rootNode 的指针和一个 nodeValue 整数作为参数,并返回一个 TreeNode 指针:

// single rotation method
func singleRotation(rootNode *TreeNode, nodeValue int) *TreeNode {
var saveNode *TreeNode
 saveNode = rootNode.LinkedNodes[opposite(nodeValue)]
 rootNode.LinkedNodes[opposite(nodeValue)] = saveNode.LinkedNodes[nodeValue]
 saveNode.LinkedNodes[nodeValue] = rootNode
 return saveNode
}

双重旋转方法

在这里,doubleRotation 方法将节点旋转两次。该方法返回一个 TreeNode 指针,接受参数如 rootNode,它是一个 treeNode 指针,以及 nodeValue,它是一个整数。如下代码所示:

// double rotation
func doubleRotation(rootNode *TreeNode, nodeValue int) *TreeNode {
var saveNode *TreeNode
 saveNode = rootNode.LinkedNodes[opposite(nodeValue)].LinkedNodes[nodeValue]
rootNode.LinkedNodes[opposite(nodeValue)].LinkedNodes[nodeValue] = saveNode.LinkedNodes[opposite(nodeValue)]
 saveNode.LinkedNodes[opposite(nodeValue)] = rootNode.LinkedNodes[opposite(nodeValue)]
 rootNode.LinkedNodes[opposite(nodeValue)] = saveNode
saveNode = rootNode.LinkedNodes[opposite(nodeValue)]
 rootNode.LinkedNodes[opposite(nodeValue)] = saveNode.LinkedNodes[nodeValue]
 saveNode.LinkedNodes[nodeValue] = rootNode
 return saveNode
}

该方法的实现如 插入节点方法 部分所示,如下。

调整平衡方法

adjustBalance 方法调整树的平衡。在以下代码片段中,adjustBalance 方法根据平衡因子、rootNodenodeValue 执行双旋转。adjustBalance 方法接受 rootNodeTreeNode 类型的实例)、nodeValuebalanceValue(都是整数)作为参数:

// adjust balance method 
func adjustBalance(rootNode *TreeNode, nodeValue int, balanceValue int) {
 var node *TreeNode
 node = rootNode.LinkedNodes[nodeValue]
 var oppNode *TreeNode
 oppNode = node.LinkedNodes[opposite(balanceValue)]
 switch oppNode.BalanceValue {
 case 0:
 rootNode.BalanceValue = 0
 node.BalanceValue = 0
 case balanceValue:
 rootNode.BalanceValue = -balanceValue
 node.BalanceValue = 0
 default:
 rootNode.BalanceValue = 0
 node.BalanceValue = balanceValue
 }
 oppNode.BalanceValue= 0
}

BalanceTree 方法

BalanceTree 方法通过单次或双旋转来改变平衡因子。该方法接受 rootNode(一个 TreeNode 指针)和 nodeValue(一个整数)作为参数。BalanceTree 方法返回一个 TreeNode 指针,如下所示:

// BalanceTree method
func BalanceTree(rootNode *TreeNode, nodeValue int) *TreeNode {
 var node *TreeNode
 node = rootNode.LinkedNodes[nodeValue]
 var balance int
 balance = 2*nodeValue - 1
 if node.BalanceValue == balance {
 rootNode.BalanceValue = 0
 node.BalanceValue = 0
 return singleRotation(rootNode, opposite(nodeValue))
 }
 adjustBalance(rootNode, nodeValue, balance)
 return doubleRotation(rootNode, opposite(nodeValue))
}

insertRNode 方法

insertRNode 方法插入节点并平衡树。此方法使用 KeyValue 键插入 rootNode,如下代码片段所示。该方法接受 rootNode(一个 TreeNode 指针)和 key(一个整数)作为参数。如果成功插入 rootNode,则方法返回一个 TreeNode 指针和一个布尔值:

//insertRNode method
func insertRNode(rootNode *TreeNode, key KeyValue) (*TreeNode, bool) {
 if rootNode == nil {
 return &TreeNode{KeyValue: key}, false
 }
 var dir int
 dir = 0
 if rootNode.KeyValue.LessThan(key) {
 dir = 1
 }
 var done bool
 rootNode.LinkedNodes[dir], done = insertRNode(rootNode.LinkedNodes[dir], key)
 if done {
 return rootNode, true
 }
 rootNode.BalanceValue = rootNode.BalanceValue+(2*dir - 1)
 switch rootNode.BalanceValue {
 case 0:
 return rootNode, true
 case 1, -1:
 return rootNode, false
 }
 return BalanceTree(rootNode, dir), true
}

InsertNode 方法

InsertNode 方法将节点插入到 AVL 树中。此方法接受 treeNode(一个双 TreeNode 指针)和 key 值作为参数:

// InsertNode method
func InsertNode(treeNode **TreeNode, key KeyValue) {
 *treeNode, _ = insertRNode(*treeNode, key)
}

以下截图显示了 InsertNode 方法的示例输出。InsertNode 方法使用 rootNode 参数和要插入的节点调用 insertRNode 方法。rootNode 的键值为 5,要插入的节点的键值为 6。树需要平衡。

因此,下一个调用将是具有键值 8rootNode 和要插入的节点。下一步调用 rootnode,键值为 7,并插入节点。最后的调用将是 rootNodenil 和要插入的节点。检查平衡值,平衡树方法返回平衡后的树:

RemoveNode 方法

在以下代码中,RemoveNode 方法通过调用 removeRNode 方法从 AVL 树中删除元素。该方法接受 treeNode(一个双 TreeNode 指针)和 KeyValue 作为参数:

// RemoveNode method
func RemoveNode(treeNode **TreeNode, key KeyValue) {
 *treeNode, _ = removeRNode(*treeNode, key)
}

removeBalance 方法

removeBalance 方法从树中移除平衡因子。在删除节点后,此方法调整平衡因子并返回一个 treeNode 指针和一个布尔值,如果平衡被移除。该方法接受 rootNodeTreeNode 的一个实例)和 nodeValue(一个整数)作为参数。如下代码所示:

// removeBalance method
func removeBalance(rootNode *TreeNode, nodeValue int) (*TreeNode, bool) {
 var node *TreeNode
 node = rootNode.LinkedNodes[opposite(nodeValue)]
 var balance int
 balance = 2*nodeValue - 1
 switch node.BalanceValue {
 case -balance:
 rootNode.BalanceValue = 0
 node.BalanceValue = 0
 return singleRotation(rootNode, nodeValue), false
 case balance:
 adjustBalance(rootNode, opposite(nodeValue), -balance)
 return doubleRotation(rootNode, nodeValue), false
 }
 rootNode.BalanceValue = -balance
 node.BalanceValue = balance
 return singleRotation(rootNode, nodeValue), true
}

removeRNode 方法

removeRNode 方法从树中删除节点并平衡树。此方法接受 rootNode(一个 TreeNode 指针)和 key 值。如果成功删除 RNode,则此方法返回一个 TreeNode 指针和一个布尔值,如下代码片段所示:

//removeRNode method
func removeRNode(rootNode *TreeNode, key KeyValue) (*TreeNode, bool) {
 if rootNode == nil {
 return nil, false
 }
 if rootNode.KeyValue.EqualTo(key) {
 switch {
 case rootNode.LinkedNodes[0] == nil:
 return rootNode.LinkedNodes[1], false
 case rootNode.LinkedNodes[1] == nil:
 return rootNode.LinkedNodes[0], false
 }
 var heirNode *TreeNode
 heirNode = rootNode.LinkedNodes[0]
 for heirNode.LinkedNodes[1] != nil {
 heirNode = heirNode.LinkedNodes[1]
 }
 rootNode.KeyValue = heirNode.KeyValue
 key = heirNode.KeyValue
 }
 var dir int
 dir = 0
 if rootNode.KeyValue.LessThan(key) {
 dir = 1
 }
 var done bool
 rootNode.LinkedNodes[dir], done = removeR(rootNode.LinkedNodes[dir], key)
 if done {
 return rootNode, true
 }
 rootNode.BalanceValue = rootNode.BalanceValue + (1 - 2*dir)
 switch rootNode.BalanceValue {
 case 1, -1:
 return rootNode, true
 case 0:
 return rootNode, false
 }
 return removeBalance(rootNode, dir)
}
type integerKey int
func (k integerKey) LessThan(k1 KeyValue) bool { return k < k1.(integerKey) }
func (k integerKey) EqualTo(k1 KeyValue) bool { return k == k1.(integerKey) }

以下展示了 removeRNode 方法的示例输出。RemoveNode 方法调用 removeRNode 方法。removeRNode 方法接受节点的参数,例如 rootNodeKeyValue

图片

main方法

在以下代码片段中,main方法通过插入具有5387610键的节点来创建 AVL 树。具有37键的节点被移除。将树数据结构转换为字节的 JSON。在转换为字符串后打印 JSON 字节:

//main method
func main() {
  var treeNode *TreeNode
  fmt.Println("Tree is empty")
  var avlTree []byte
  avlTree, _ = json.MarshalIndent(treeNode, "", " ")
  fmt.Println(string(avlTree))

  fmt.Println("\n Add Tree")
  InsertNode(&treeNode, integerKey(5))
  InsertNode(&treeNode, integerKey(3))
  InsertNode(&treeNode, integerKey(8))
  InsertNode(&treeNode, integerKey(7))
  InsertNode(&treeNode, integerKey(6))
  InsertNode(&treeNode, integerKey(10))
  avlTree, _ = json.MarshalIndent(treeNode, "", " ")
  fmt.Println(string(avlTree))

  fmt.Println("\n Delete Tree")
  RemoveNode(&treeNode, integerKey(3))
  RemoveNode(&treeNode, integerKey(7))
  avlTree, _ = json.MarshalIndent(treeNode, "", " ")
  fmt.Println(string(avlTree))
}

运行以下命令以执行avl_tree.go文件:

go run avl_tree.go

输出如下:

图片

在下一节中,将讨论 B+树的实现并展示代码片段。

B+树

B+树包含一个键列表和指向树中下一级节点的指针。在搜索过程中,通过查找相邻节点键来递归搜索元素。B+树用于在文件系统中存储数据。B+树在树中搜索节点时需要的 I/O 操作更少。扇出定义为指向 B+树中节点子节点的节点数。B+树首先由鲁道夫·拜尔和爱德华·M·麦克雷特在技术论文中描述。

B+树中的面向块存储上下文有助于数据的存储和高效检索。通过使用压缩技术可以增强 B+树的空间效率。B+树属于多路搜索树家族。对于 b 阶 B+树,空间使用量是n的阶。插入、查找和删除操作是b的对数阶。

B 树

B 树是一种搜索树,其非叶节点只包含键,而数据位于叶节点中。B 树用于减少磁盘访问次数。B 树是一种自我调整的数据结构,能够保持数据的排序。B 树以排序顺序存储键,以便于遍历。它们可以处理多个插入和删除操作。

计算机科学家高德纳(Knuth)最初提出了这种数据结构的概念。B 树由最多有n个子节点的节点组成。树中的每个非叶节点至少有n/2 个子节点。鲁道夫·拜尔(Rudolf Bayer)和爱德华·M·麦克雷特(Edward M. McCreight)首先在他们的工作中实现了这种数据结构。B 树用于 HFS 和 Reiser4 文件系统,以便快速访问文件中的任何块。平均而言,空间使用量是n的阶。插入、搜索和删除操作是n的对数阶。

T 树

T 树是一种平衡的数据结构,它将索引和实际数据都存储在内存中。它们用于内存数据库。T 指的是节点的形状。每个节点由指向父节点和左右子节点的指针组成。树中的每个节点都将有一个有序的数据指针数组和额外的控制数据。

T 树与内存中的树结构具有相似的性能优势。T 树是在自平衡二叉搜索树之上实现的。这种数据结构适合于有序扫描数据。它支持各种程度的隔离。

表格

如我们所知,表格在数据管理和其他领域中被使用。一个表格有一个名称和包含列名的表头。让我们看看以下章节中表格的不同类,如Table类、Row类、Column类以及PrintTable方法。

对于本节,请参考table.go文件。

Table 类

Table类有一个行数组和列名数组。表格的Namestruct类中的一个字符串属性,如下所示:

// Table Class
type Table struct {
    Rows []Row
    Name string
    ColumnNames []string
}

Row 类

Row类有一个列数组和一个整型Id,如下面的代码所示。Id实例是行的唯一标识符:

// Row Class
type Row struct {
 Columns []Column
 Id int
}

Column 类

Column类有一个整型Id和一个由唯一标识符识别的Value字符串,如下面的代码片段所示:

// Column Class
type Column struct {
 Id int
 Value string
}

打印表格方法

在以下代码片段中,printTable方法打印表格的行和列。遍历行,然后对于每一行打印列:

//printTable
func printTable(table Table){
 var rows []Row = table.Rows
 fmt.Println(table.Name)
 for _,row := range rows {
 var columns []Column = row.Columns
 for i,column := range columns {
 fmt.Println(table.ColumnNames[i],column.Id,column.Value);
 }
 }
}

main 方法

在这个main方法中,我们将实例化我们刚刚看到的类,如TableRowColumnmain方法创建一个表格并设置名称和列名。列用值创建。在创建行之后,将列设置在行上。通过调用printTable方法打印表格,如下所示:

// main method
func main() {
 var table Table = Table{}
 table.Name = "Customer"
 table.ColumnNames = []string{"Id", "Name","SSN"}
 var rows []Row = make([]Row,2)
 rows[0] = Row{}
 var columns1 []Column = make([]Column,3)
 columns1[0] = Column{1,"323"}
 columns1[1] = Column{1,"John Smith"}
 columns1[2] = Column{1,"3453223"}
 rows[0].Columns = columns1
 rows[1] = Row{}
 var columns2 []Column = make([]Column,3)
 columns2[0] = Column{2,"223"}
 columns2[1] = Column{2,"Curran Smith"}
 columns2[2] = Column{2,"3223211"}
 rows[1].Columns = columns2
 table.Rows = rows
 fmt.Println(table)
 printTable(table)
}

运行以下命令以执行table.go文件:

go run table.go

输出如下:

图片

下一节将讨论符号表数据结构。

符号表

在程序翻译过程中,内存中存在符号表。它也可以存在于程序二进制文件中。符号表包含符号的名称、位置和地址。在 Go 语言中,gosym包实现了对 Go 符号和行号表的访问。由 GC 编译器生成的 Go 二进制文件具有符号和行号表。行表是一种将程序计数器映射到行号的数据结构。

容器

容器包提供了对 Go 中堆、列表和环形功能的访问。容器用于社交网络、知识图谱和其他领域。容器是列表、映射、切片、通道、堆、队列和 treaps。列表在第一章 数据结构和算法 中介绍。映射和切片是 Go 中的内置容器。Go 中的通道被称为队列。堆是一种树形数据结构。这种数据结构满足堆属性。队列在第三章 线性数据结构 中被建模为堆。treap 是树和堆的结合。它是一个具有键和值的二叉树,并维护一个堆来保持优先级。

环被称为循环链表,将在下一节中介绍。对于本节,请参考circular_list.go文件。

循环链表

循环链表是一种数据结构,其中最后一个节点后面跟着第一个节点。使用container/ring结构来模拟循环链表。以下是一个循环链表的实现示例:

package main
import (
 "container/ring"
 "fmt"
)
func main() {
 var integers []int
 integers = []int{1,3,5,7}
 var circular_list *ring.Ring
 circular_list= ring.New(len(integers))
 var i int
 for i = 0; i < circular_list.Len(); i++ {
 circular_list.Value = integers[i]
 circular_list = circular_list.Next()
 }

使用参数len nring.New方法创建长度为n的循环列表。通过使用Next方法遍历circular_list,使用整数数组初始化循环链表。ring.Ring类的Do方法接受一个元素作为接口,并按以下方式打印元素:

circular_list.Do(func(element interface{}) {
 fmt.Print(element,",")
 })
 fmt.Println()

使用Prev方法遍历循环列表的逆序,并在以下代码中打印其值:

// reverse of the circular list
 for i = 0; i < circular_list.Len(); i++ {
 fmt.Print(circular_list.Value,",")
 circular_list = circular_list.Prev()
 }
 fmt.Println()

在以下代码片段中,使用Move方法将循环列表向前移动两个元素,并打印其值:

// move two elements forward in the circular list
 circular_list = circular_list.Move(2)
 circular_list.Do(func(element interface{}) {
 fmt.Print(element,",")
 })
 fmt.Println()
}

运行以下命令以执行circular_list.go文件:

go run circular_list.go

输出如下:

图片

下一节将讨论hash函数数据结构。

哈希函数

哈希函数在密码学和其他领域中被使用。这些数据结构通过与密码学相关的代码示例进行展示。在 Go 语言中实现hash函数有两种方式:使用crc32sha256。序列化(将字符串转换为编码形式)保存了内部状态,这将在以后用于其他目的。本节解释了BinaryMarshaler(将字符串转换为二进制形式)的示例:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main
// importing bytes, crpto/sha256, encoding, fmt and log package
import (
 "bytes"
 "crypto/sha256"
 "encoding"
 "fmt"
 "log"
 "hash"
)

main方法创建了两个示例字符串的二进制序列化哈希。打印两个字符串的哈希值。使用字节上的 equals 方法比较第一个哈希的总和与第二个哈希。这在上面的代码中展示:

//main method
func main() {
 const (
 example1 = "this is a example "
 example2 = "second example"
 )
 var firstHash hash.Hash
 firstHash = sha256.New()
 firstHash.Write([]byte(example1))
 var marshaler encoding.BinaryMarshaler
 var ok bool
 marshaler, ok = firstHash.(encoding.BinaryMarshaler)
 if !ok {
 log.Fatal("first Hash is not generated by encoding.BinaryMarshaler")
 }
 var data []byte
 var err error
 data, err = marshaler.MarshalBinary()
 if err != nil {
 log.Fatal("failure to create first Hash:", err)
 }
 var secondHash hash.Hash
 secondHash = sha256.New()
var unmarshaler encoding.BinaryUnmarshaler
 unmarshaler, ok = secondHash.(encoding.BinaryUnmarshaler)
 if !ok {
 log.Fatal("second Hash is not generated by encoding.BinaryUnmarshaler")
 }
 if err := unmarshaler.UnmarshalBinary(data); err != nil {
 log.Fatal("failure to create hash:", err)
 }
 firstHash.Write([]byte(example2))
 secondHash.Write([]byte(example2))
 fmt.Printf("%x\n", firstHash.Sum(nil))
 fmt.Println(bytes.Equal(firstHash.Sum(nil), secondHash.Sum(nil)))
}

运行以下命令以执行hash.go文件:

go run hash.go

输出如下:

图片

摘要

本章介绍了树、二叉搜索树和 AVL 树。简要解释了 Treap、B 树和 B+树。通过各种代码示例展示了在树中进行插入、删除和更新元素的操作。在最后一节中介绍了表格、容器和哈希函数。在每个部分中解释了插入、删除和搜索等操作的时间和空间复杂度。

在下一章中,将介绍二维和多维数组等齐次数据结构。

问题

  1. 你能给出一个可以使用二叉搜索树的例子吗?

  2. 在二叉搜索树中搜索元素使用的是哪个方法?

  3. 在 AVL 树中调整平衡使用了哪些技术?

  4. 什么是符号表?

  5. 在哈希类中调用哪个类和方法来生成二进制序列化的哈希?

  6. Go 语言中哪个容器用于模拟循环链表?

  7. 如何从树结构创建 JSON(缩进)?使用了哪个类和方法?

  8. 如何比较哈希的总和?

  9. AVL 树中的平衡因子是什么?

  10. 你如何在表格中识别行和列?

进一步阅读

如果你想了解更多关于树、二叉搜索树和 AVL 树的内容,以下书籍推荐:

  • 《设计模式》,作者:Erich Gamma, Richard Helm, Ralph Johnson, 和 John Vlissides

  • 《算法导论(第三版)》,作者:Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein

  • 《数据结构与算法:简易入门》,作者:Rudolph Russell

第五章:同质数据结构

同质数据结构包含类似类型的数据,例如整数或双精度值。同质数据结构用于矩阵、张量和向量数学。张量是标量和向量的数学结构。一阶张量是一个向量。向量由一行或一列组成。零阶张量是一个标量矩阵是数字的二维集合。它们都用于科学分析。

张量在材料科学中得到了应用。它们在数学、物理、力学、电动力学和广义相对论中都有使用。机器学习解决方案利用张量数据结构。张量具有位置、形状和静态大小等属性。

本章涵盖了以下同质数据结构:

  • 二维数组

  • 多维数组

以下场景展示了二维和多维数组的用法:

  • 矩阵表示

  • 乘法

  • 加法

  • 减法

  • 矩阵行列式计算

  • 逆运算

  • 转置

技术要求

为您的操作系统安装 Go 版本 1.10,请访问golang.org/doc/install

本章代码的 GitHub URL 如下:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter05.

二维数组

二维数组在第二章,使用 Go 进行数据结构和算法入门中简要介绍。为了回顾,对于动态分配,我们使用切片的切片,它是一个二维数组。二维数组是一个单维数组的列表。二维数组arr中的每个元素都标识为arr[i][j],其中arr是数组的名称,ij分别代表行和列,它们的值分别从 0 到m和从 0 到n。遍历二维数组是O(mn*)复杂度。

以下代码展示了如何初始化一个数组:

var arr = [4][5] int{
    {4,5,7,8,9},
    {1,2,4,5,6},
    {9,10,11,12,14},
    {3,5,6,8,9}
}

在二维数组中,通过行索引和列索引来访问元素。在以下示例中,检索数组中第2行和第3列的值作为一个整数值:

var value int = arr[2][3]

数组可以存储相同类型的数据元素的顺序集合。同质数据结构数组由连续的内存地址位置组成。

二维矩阵被建模为二维数组。标量是定义向量空间的域中的一个元素。矩阵可以乘以一个标量。你可以用任何非零实数除以一个矩阵。

矩阵的阶数是行数m和列数n的乘积。具有m行和n列的矩阵被称为m x n矩阵。存在多种类型的矩阵,例如行矩阵列矩阵三角矩阵零矩阵零矩阵;让我们在以下章节中讨论它们。

行矩阵

行矩阵是一个由单个行m元素组成的 1 x m矩阵,如下所示:

var matrix = [1][3] int{
    {1, 2, 3}
}

执行以下命令以运行row_matrix.go文件:

go run row_matrix.go

输出如下:

图片

下一节将讨论列矩阵数据结构。

列矩阵

列矩阵是一个m x 1 矩阵,它有一个由m元素组成的单列。以下代码片段展示了如何创建列矩阵:

var matrix = [4][1] int{
    {1},
    {2},
    {3},
    {4}
}

执行以下命令以运行column_matrix.go文件:

go run column_matrix.go

输出如下:

图片

下一节将讨论下三角矩阵数据结构。

下三角矩阵

下三角矩阵由主对角线上方的元素值为零的元素组成。以下代码片段展示了如何创建一个下三角矩阵:

var matrix = [3][3] int{
    {1,0,0},
    {1,1,0},
    {2,1,1}
}

执行以下命令以运行lower_triangular.go文件:

go run lower_triangular.go

输出如下:

图片

下一节将讨论上三角矩阵数据结构。

上三角矩阵

上三角矩阵由主对角线下方的元素值为零的元素组成。以下代码创建了一个上三角矩阵:

var matrix = [3][3] int{
    {1,2,3},
    {0,1,4},
    {0,0,1}
}

执行以下命令以运行upper_triangular.go文件:

go run upper_triangular.go

输出如下:

图片

下一节将讨论零矩阵数据结构。

零矩阵

零矩阵或零矩阵是一个完全由零值组成的矩阵,如下面的代码片段所示:

var matrix = [3][3] int{
    {0,0,0},
    {0,0,0},
    {0,0,0}
}

执行以下命令以运行null_matrix.go文件:

go run null_matrix.go

输出如下:

图片

下一节将讨论单位矩阵数据结构。

单位矩阵

单位矩阵是一个单位矩阵,其主对角线上的元素为 1,其他位置的元素为 0。以下代码片段创建了一个单位矩阵:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)
// identity method 
func Identity(order int) [][]float64 {
var matrix [][]float64
    matrix = make([][]float64, order)
var i int
    for i = 0; i < order; i++ {
var temp []float64
        temp = make([]float64, order)
        temp[i] = 1
        matrix[i] = temp
    }
    return matrix
}
// main method 
func main() {
    fmt.Println(Identity(4))
}

执行以下命令以运行前面的代码片段:

go run identity_matrix.go

输出如下:

图片

下一节将讨论对称矩阵数据结构。

对称矩阵

对称矩阵是一个转置后等于自身的矩阵。对称矩阵包括其他类型的矩阵,如反对称矩阵中心对称矩阵循环矩阵协方差矩阵科克斯特矩阵汉克尔矩阵希尔伯特矩阵反对称矩阵斜对称矩阵托普利茨矩阵负矩阵是指每个元素都是负数的矩阵。

基本二维矩阵运算

在本节中,我们将查看二维矩阵的基本操作。让我们从初始化矩阵开始。

以下代码片段初始化了 matrix1matrix2

var matrix1 = [2][2] int{
    {4,5},
    {1,2}
}
var matrix2 = [2][2] int{
    {6,7},
    {3,4}
}

在接下来的几节中,我们将介绍 addsubtractmultiplytransposeinversion 操作。对于本节,请参考 binary_search_tree.go 文件。

add 方法

add 方法用于将两个 2 x 2 矩阵的元素相加。以下代码通过相加两个矩阵来返回创建的矩阵:

// add method
func add(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
    var m int
    var l int
    var sum [2][2]int
    for l = 0; l < 2; l++ {
        for m=0; m <2; m++ {
            sum[l][m] = matrix1[l][m] +matrix2[l][m]
        }
    }
    return sum
}

两个矩阵的和是调用 add 方法的结果。传递的参数是要相加的矩阵,如下所示:

var sum [2][2]int 
sum = add(matrix1, matrix2)

以下为 add 方法的示例输出。将 matrix1matrix2 相加得到和矩阵

subtract 方法

subtract 方法用于从两个 2 x 2 矩阵中减去元素。以下代码片段中 subtract 方法减去了 matrix1matrix2 的元素。此方法返回减法操作后的结果矩阵:

// subtract method
func subtract(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
    var m int
    var l int
    var difference [2][2]int
    for l = 0; l < 2; l++ {
        for m=0; m <2; m++ {
            difference[l][m] = matrix1[l][m] -matrix2[l][m]
        }
    }
    return difference
}

两个矩阵的差是调用 subtract 方法的结果。传递的参数是要减去的矩阵,如下所示:

var difference [2][2]int
difference = subtract(matrix1, matrix2)

以下为 subtract 方法的示例输出:

乘法方法

multiply 方法用于将两个 2 x 2 矩阵的元素相乘。以下代码片段展示了矩阵 matrix1matrix2 的乘法。乘法操作后生成的矩阵由 multiply 方法返回:

// multiply method
func multiply(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
     var m int
    var l int
    var n int
    var product [2][2]int
    for l = 0; l < 2; l++ {
        for m=0; m <2; m++ {
            var productSum int = 0
            for n=0; n< 2; n++ {
                productSum = productSum + matrix1[l][n]*matrix2[n][m]
            }
            product[l][m] = productSum;
        }
    }
    return product
}

在以下代码片段中,使用 multiply 方法计算两个矩阵的乘积,该方法接收两个矩阵作为参数:

var product [2][2]int
product = multiply(matrix1, matrix2)

乘法方法的示例输出如下。matrix1matrix2 的乘积是乘积矩阵

transpose 方法

使用 transpose 方法可以计算矩阵的转置。该方法接收矩阵作为参数,并返回转置后的矩阵:

// transpose method
func transpose(matrix1 [2][2]int) [2][2]int {
    var m intvar l int
    var transMatrix [2][2]int
    for l = 0; l < 2; l++ {
        for m=0; m <2; m++ {
            transMatrix[l][m] = matrix1[m][l]
        }
    }
    return transMatrix
}

determinant 方法

determinant 方法用于计算矩阵的行列式。以下代码片段中的 determinant 方法计算矩阵的行列式值。该方法接收矩阵作为参数,并返回一个 float32 类型的值,即矩阵的行列式:

// determinant method
func determinant(matrix1 [2][2]int) float32 {
    var m int
    var l int
    var det float32
    det = det + ( (matrix1[0][0]*matrix1[1][1])-(matrix1[0][1]*matrix1[1][0]));
    return det
}

inverse 方法

inverse 方法返回矩阵的逆,该逆矩阵作为参数传入。以下代码片段展示了这一点:

//inverse method
func inverse(matrix [2][2]int) [][]float64 {
  var det float64
  det = determinant(matrix)
  var invmatrix float64

  invmatrix[0][0] = matrix[1][1]/det
  invmatrix[0][1] = -1*matrix[0][1]/det
  invmatrix[1][0] = -1*matrix[1][0]/det
  invmatrix[1][1] = matrix[0][0]/det
  return invmatrix
}

运行以下命令以执行 twodmatrix.go 文件:

go run twodmatrix.go

输出如下:

下一节将讨论 zig-zag 矩阵数据结构。

Zig-zag 矩阵

斜杠矩阵是一个n x n整数的正方形排列。整数按顺序排列在反斜对角线上。以下代码解释了如何创建斜杠矩阵以及如何遍历它。PrintZigZag方法以顺序递增的顺序以斜杠方式创建矩阵。该方法接受整数n作为参数,并返回整数数组,即斜杠矩阵:

///main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
    "fmt"
)
//prints the matrix in zig-zag fashion
func PrintZigZag(n int) []int {
    var zigzag []int
    zigzag = make([]int, n*n)
    var i int
    i = 0
    var m int
    m = n * 2
    var  p int
    for p = 1; p <= m; p++ {
        var x int
        x = p - n
        if x < 0 {
           x = 0
        }
        var y int
        y = p - 1
        if y > n-1 {
            y = n - 1
       }
       var j int
       j = m - p
        if j > p {
            j = p
        }
        var k int
        for k = 0; k < j; k++ {
           if p&1 == 0 {
               zigzag[(x+k)*n+y-k] = i
           } else {
               zigzag[(y-k)*n+x+k] = i
            }
           i++
        }
   }
   return zigzag
}

main方法调用PrintZigZag方法,该方法接受参数n,首先从左到右打印矩阵,然后从右到左打印第二层,依此类推。整数的数量为5,字段宽度为2

// main method
func main() {
  var n int    
  n = 5
  var length int
    length = 2
    var i int
    var sketch int
    for i, sketch = range PrintZigZag(n) {
       fmt.Printf("%*d ", length, sketch)
        if i%n == n-1 {
            fmt.Println("")
       }
   }
}

运行以下命令以执行zigzagmatrix.go文件:

go run zigzagmatrix.go

输出如下:

下一节将讨论螺旋矩阵数据结构。

螺旋矩阵

螺旋矩阵是一个n x n整数的排列,其中整数按顺序螺旋式递增排列。螺旋矩阵是一个古老的玩具算法。使用四个循环来保持螺旋顺序,每个循环对应矩阵的一个角落。以下代码片段中的PrintSpiral方法创建了一个元素按递增顺序螺旋排列的矩阵。该方法接受一个参数n,并返回一个整数数组:

///main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
    "fmt"
)
//PrintSpiral method
func PrintSpiral(n int) []int {

    var left int
    var top int
    var right int
    var bottom int

    left =0
    top =0
    right = n-1
    bottom = n-1
    var size int
    size = n * n
    var s []int
    s = make([]int, size)

    var i int
    i = 0
    for left < right {

        var c int
        for c = left; c <= right; c++ {
            s[top*n+c] = i
            i++
        }
        top++

        var r int
        for r = top; r <= bottom; r++ {
            s[r*n+right] = i
            i++
        }
        right--
        if top == bottom {
            break
        }

        for c = right; c >= left; c-- {
            s[bottom*n+c] = i
            i++
        }
        bottom--

        for r = bottom; r >= top; r-- {
            s[r*n+left] = i
            i++
        }
        left++
    }

    s[top*n+left] = i

    return s
}

在以下代码片段中,main方法调用PrintSpiral方法,该方法接受整数n并按螺旋方式打印矩阵的整数值。PrintSpiral方法返回的值以宽度为2的字段打印:

func main() {
   var n int
    n = 5
  var length int
    length = 2
    var i int
    var sketch int
    for i, sketch = range PrintSpiral(n) {
        fmt.Printf("%*d ", length, sketch)
        if i%n == n-1 {
            fmt.Println("")
        }
    }
}

运行以下命令以执行spiralmatrix.go文件:

go run spiralmatrix.go

输出如下:

下一节将讨论布尔矩阵数据结构。

布尔矩阵

布尔矩阵是一个矩阵,它由第m行和第n列的元素组成,这些元素的值为 1。可以通过将第m行和第n列的值设置为 1 来将矩阵修改为布尔矩阵。在以下代码中,布尔矩阵转换和打印方法被详细展示。changeMatrix方法通过将单元格值从 0 更改为 1 来将输入矩阵转换为布尔矩阵。该方法接受输入矩阵作为参数,并返回更改后的矩阵,如下所示:


///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)
//changeMatrix method
func changeMatrix(matrix [3][3]int) [3][3]int {
   var i int
   var j int
   var Rows [3]int
   var Columns [3]int

   var matrixChanged [3][3]int

   for i=0; i<3; i++{
     for j=0; j < 3; j++{
         if matrix[i][j]==1 {
            Rows[i] =1
            Columns[j] =1
         }

      }
    }

   for i=0; i<3; i++ {
    for j=0; j<3; j++{
      if Rows[i]==1 || Columns[j]==1{
      matrixChanged[i][j] = 1
      }

     }
  }

  return matrixChanged

}

以下截图显示了更改矩阵方法的示例输出。检查行或列中的值为 1 的元素,并将行元素更新为 1:

让我们来看看printMatrix方法和main方法。

printMatrix方法

在以下代码片段中,printMatrix方法接受输入矩阵,并按行打印矩阵值,并为每一行遍历列:

//printMatrix method
func printMatrix(matrix [3][3]int) {
   var i int
   var j int
   //var k int
   for i=0; i < 3; i++ {

     for j=0; j < 3; j++ {

          fmt.Printf("%d",matrix[i][j])

     }
     fmt.Printf("\n")
   }

}

main方法

在以下代码片段中,main方法在初始化矩阵后调用changeMatrix方法。在调用changeMatrix方法后打印了更改后的矩阵:

//main method
func main() {

 var matrix = [3][3] int {{1,0,0},{0,0,0},{0,0,0}}

 printMatrix(matrix)

 matrix = changeMatrix(matrix)

 printMatrix(matrix)

}

运行以下命令以执行boolean_matrix.go文件:

go run boolean_matrix.go

输出如下:

图片

下一个部分将讨论多维数组。

多维数组

数组是数据元素的同构集合。数组的索引范围从索引 0 到索引m-1,其中m是数组的固定长度。多维数组是一个数组的数组。以下代码初始化了一个多维数组。以下打印了一个三维数组:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
    "math/rand"
)
//main method
func main() {

var threedarray [2][2][2]int

var i int

var j int

var k int

for i=0; i < 2; i++ {

   for j=0; j < 2; j++ {

     for k=0; k < 2; k++ {

         threedarray[i][j][k] = rand.Intn(3)
     }
   }
}

 fmt.Println(threedarray)
}

运行以下命令以执行前面的代码片段:

go run multidarray.go

输出如下:

图片

下一个部分将讨论张量数据结构。

张量

张量是由空间坐标组成的分量构成的多维数组。张量在物理学和生物学的电磁学和扩散张量成像等主题中被广泛使用。威廉·罗文·哈密顿是第一个提出“张量”这个术语的人。张量在抽象代数和代数拓扑中扮演着基本角色。

张量阶数是其参数阶数之和,加上结果张量的阶数。例如,惯性矩阵是一个二阶张量。自旋也是多维数组,但它们的元素值通过坐标变换而改变。

张量的初始化如下所示。数组使用从 0 到 3 的整数值初始化:

var array [3][3][3]int
var i int
var j int
var k int
for i=0; i < 3; i++ {
   for j=0; j < 3; j++ {
     for k=0; k < 3; k++ {

         array[i][j][k] = rand.Intn(3)
     }
   }
}

张量的展开是沿着第一维进行的。重新排列张量模式的n个向量被称为张量的n-展开。以下展示了张量数组的 0 模式展开:

   for j=0; j < 3; j++ {
     for k=0; k < 3; k++ {
         fmt.Printf("%d ",array[0][j][k])
     }
     fmt.Printf("\n")
   }

以下展示了张量数组的 1 模式展开。数组的第一个维度索引被设置为 1:

   for j=0; j < 3; j++ {
        for k=0; k < 3; k++ {
            fmt.Printf("%d ",array[1][j][k])
        }
        fmt.Printf("\n")
      }

以下展示了张量数组的 2 模式展开。数组的第一个维度行索引被设置为 2:

for j=0; j < 3; j++ {
           for k=0; k < 3; k++ {
               fmt.Printf("%d ",array[2][j][k])
           }
           fmt.Printf("\n")
         }

运行以下命令以执行tensor.go文件:

go run tensor.go

输出如下:

图片

摘要

本章介绍了诸如二维数组和多维数组等同构数据结构。使用代码示例解释了矩阵运算,如求和、减法、乘法、逆和行列式。使用二维数组解释了螺旋矩阵、之字形矩阵和布尔矩阵。还介绍了张量以及折叠等操作。

在下一章中,将介绍诸如链表、有序列表和无序列表等异构数据结构。

问题

  1. 张量数组的 2 模式展开是什么?

  2. 编写一个字符串二维数组并初始化它。打印字符串。

  3. 给出一个多维数组的示例并遍历它。

  4. 对于一个 3 x 3 矩阵,编写计算矩阵行列式的代码。

  5. 3 x 3 矩阵的转置是什么?

  6. 什么是之字形矩阵?

  7. 编写一个螺旋矩阵的示例代码。

  8. 张量数组通常展开哪个维度?

  9. 你如何定义布尔矩阵?

  10. 选择两个 3 x 3 矩阵并找出矩阵的乘积。

进一步阅读

如果你想了解更多关于数组、矩阵和张量的知识,以下书籍推荐:

  • 《高级数据结构》,作者 Peter Brass

  • 《动态数据结构:列表、栈、队列和树》,作者 Bogdan Patrut 和 Tiberiu Socaciu

  • 《数据结构与算法:简易入门》,作者 Rudolph Russell

第六章:异构数据结构

异构数据结构是包含多种类型数据的数据结构,例如整数、双精度浮点数和浮点数。链表有序列表是这些数据结构的良好示例。它们用于内存管理。链表是由指针关联的元素链。每个元素的指针链接到下一个项目,从而将链连接在一起。链表不必占用一块内存。它们使用的内存可以动态分配。它由一系列节点组成,这些节点是列表的组成部分。为了展示列表和存储管理,展示了 HTML 中的有序列表和无序列表。我们将在本章中介绍链表、有序列表和无序列表,并使用适当的示例展示它们的实现方法。本章涵盖了以下异构数据结构:

  • 链表

  • 有序列表

  • 无序列表

我们在第三章中通过代码示例介绍了单链表和双链表,线性数据结构。循环链表在第四章中介绍,非线性数据结构

技术要求

从以下链接安装适用于您的操作系统的 Go 版本 1.10:golang.org/doc/install

本章代码的 GitHub URL 如下:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter06

链表

链表是由具有信息的元素组成的线性集合。链表根据是否包含或删除组件而收缩或扩展。这个列表可以很小或很大,但无论大小,组成它的元素都是简单的。链表在第三章中介绍,线性数据结构。它们比数组消耗更多的内存。对于单链表来说,反向遍历是一个问题,因为单链表指向下一个节点是向前。下一节将解释如何通过代码示例来反转单链表。

单链表、双链表和循环链表将在本章中介绍。

单链表

单链表是一种动态数据结构,其中添加和删除操作很容易;这是因为它是动态数据结构,不是固定的。栈和队列数据结构是用链表实现的。当动态添加元素时,会消耗更多内存,因为动态数据结构不是固定的。单链表不支持随机检索,因为您需要遍历节点以定位节点。单链表中的插入可以在列表的开始或结束处,以及指定节点之后。删除可以在列表的开始或结束处以及指定节点之后进行。

本节展示了如何反转单链表。本节中解释的方法是代码包中提供的 linked_list.go 文件的一部分。

在此代码片段中定义了 Node 类,具有节点指针 nextNoderune 属性:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)

// Node struct
type Node struct {
    nextNode *Node
    property rune
}

下一个部分将讨论单链表的方法。

创建链表方法

CreateLinkedList 方法创建从 az 的 runes 链表:

// create List method
func CreateLinkedList() *Node {
    var headNode *Node
    headNode = &Node{nil, 'a'}
    var currNode *Node
    currNode = headNode
    var i rune
    for i= 'b'; i <= 'z'; i++ {
        var node *Node
        node = &Node{nil, i}
        currNode.nextNode = node
        currNode = node
    }
    return headNode
}

以下为 CreateLinkedList 方法的示例输出。headNode 以值 97 创建。链表以从 az 的节点创建:

逆序链表方法

ReverseLinkedList 函数接受一个节点指针 nodeList 并返回一个指向反转链表的节点指针。

以下代码片段展示了如何反转链表:

// Reverse List method
func ReverseLinkedList(nodeList *Node) *Node {
    var currNode *Node
    currNode = nodeList
    var topNode *Node = nil
    for {
        if currNode == nil {
            break
        }
        var tempNode *Node
        tempNode = currNode.nextNode
        currNode.nextNode = topNode
        topNode = currNode
        currNode = tempNode
    }
    return topNode
}

以下为逆序链表方法的示例输出。该方法接受从 az 的链字符串参数。反转后的列表是从 za 的节点:

主方法

main 方法创建链表,并以字符串格式打印链表和反转后的链表:

// main method
func main() {
    var linkedList = CreateLinkedList()
    StringifyList(linkedList)
    StringifyList(ReverseLinkedList(linkedList))
}

执行以下命令以运行 linked_list.go 文件:

go run linked_list.go

这是输出:

下一个部分将讨论双向链表数据结构。

双向链表

双向链表是一种由具有指向前一个和下一个节点的节点组成的数据结构。在 第三章 线性数据结构 中展示了双向链表的代码示例。Go 中的列表实现为双向链表。元素 141 分别向后和向前推送。元素 65 分别插入到前后。双向链表被迭代,并打印出元素。本节中的代码展示了如何使用列表:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and list package
import (
    "container/list"
    "fmt"
)

// main method
func main() {  
    var linkedList *list.List
    linkedList = list.New()
    var element *list.Element
    element = linkedList.PushBack(14)
    var frontElement *list.Element
    frontElement = linkedList.PushFront(1)
    linkedList.InsertBefore(6, element)
    linkedList.InsertAfter(5, frontElement)

    var currElement *list.Element
    for currElement = linkedList.Front(); currElement != nil; currElement = 
    currElement.Next() {
        fmt.Println(currElement.Value)
    }
}

执行以下命令以运行 double_linked_list.go 文件:

go run double_linked_list.go

这是输出:

下一个部分将讨论循环链表数据结构。

循环链表

循环链表是一系列节点集合,其中最后一个节点连接到第一个节点。循环链表在 第四章 中简要介绍,非线性数据结构。循环链表用于创建循环队列。

在以下章节中,定义并实现了循环队列结构。本节中解释的方法是代码包中给出的 circular_queue.go 文件的一部分。

CircularQueue

CircularQueue 类具有 sizeheadlast 整数属性,以及一个 nodes 数组。该类在以下代码片段中定义:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)

//Circular Queue
type CircularQueue struct {
    size int
    nodes []interface{}
    head int
    last int
}

让我们在以下章节中讨论 CircularQueue 类的不同方法。

NewQueue 方法

NewQueue 方法创建循环队列的新实例。NewQueue 函数接受 num 参数,它是队列的 size。函数返回节点组成的循环队列,如下面的代码所示:

// NewCircularQueue method
func NewQueue(num int) *CircularQueue {
    var circularQueue CircularQueue
    circularQueue = CircularQueue{size: num + 1, head: 0, last: 0}
    circularQueue.nodes = make([]interface{}, circularQueue.size)
    return &circularQueue
}

IsUnUsed 方法

在以下代码片段中,CircularQueue 类的 IsUnUsed 方法检查 head 是否等于 last 节点,如果是则返回 true;否则返回 false

// IsUnUsed method
func (circularQueue CircularQueue) IsUnUsed() bool {
    return circularQueue.head == circularQueue.last
}

IsComplete 方法

CircularQueue 类的 IsComplete 方法如果头节点位置与 last 节点位置 +1 相同则返回 true;否则返回 false

// IsComplete method
func (circularQueue CircularQueue) IsComplete() bool {
    return circularQueue.head == (circularQueue.last+1)%circularQueue.size
}

Add 方法

此方法将给定的元素添加到循环队列中。在以下代码片段中,Add 方法接受接口类型的 element 参数并将 element 添加到循环队列中:

// Add method
func (circularQueue *CircularQueue) Add(element interface{}) {
    if circularQueue.IsComplete() {
        panic("Queue is Completely Utilized")
    }
    circularQueue.nodes[circularQueue.last] = element
    circularQueue.last = (circularQueue.last + 1) % circularQueue.size
}

Add 方法的示例输出如下。Add 方法接受值为 1element 并更新队列:

图片

MoveOneStep 方法

MoveOneStep 方法将 element 在循环队列中向前移动一步。MoveOneStep 方法接受接口类型的 element 参数,并将 element 设置为 head 节点后,将 head 节点移动到位置二:

//MoveOneStep method
func (circularQueue *CircularQueue) MoveOneStep() (element interface{}) {
    if circularQueue.IsUnUsed() {
        return nil
    }
    element = circularQueue.nodes[circularQueue.head]
    circularQueue.head = (circularQueue.head + 1) % circularQueue.size
    return
}

main 方法

main 方法创建队列并向循环队列添加元素:

// main method
func main() {
   var circularQueue *CircularQueue
   circularQueue = NewQueue(5)
   circularQueue.Add(1)
   circularQueue.Add(2)
   circularQueue.Add(3)
   circularQueue.Add(4)
   circularQueue.Add(5)
   fmt.Println(circularQueue.nodes)

}

运行以下命令以执行 circular_queue.go 文件:

go run circular_queue.go

这是输出:

图片

在以下章节中,将使用代码示例解释有序列表和无序列表。

有序列表

Go 中的列表可以按两种方式排序:

  • 有序列表:通过为切片数据类型创建一组方法并调用 sort

  • 无序列表:另一种方式是调用 sort.Slice 并使用自定义的 less 函数

有序列表和无序列表之间的唯一区别是,在有序列表中,显示项目顺序是强制性的。

HTML 中的有序列表以 <ol> 标签开始。列表中的每个项目都写在 <li> 标签中。以下是一个示例:

<ol>
    <li>Stones</li>
    <li>Branches</li>
    <li>Smoke</li>
</ol>

以下代码片段展示了使用 Golang 的有序列表示例。Employee类具有NameIDSSNAge属性:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and sort package
import (
    "fmt"
    "sort"
)

// class Employee
type Employee struct {
    Name string
    ID string
    SSN int
    Age int
}

以下章节中解释的方法是代码包中提供的linked_list.go文件的一部分。

ToString方法

Employee类的ToString方法返回员工的字符串版本。字符串版本由逗号分隔的NameAgeIDSSN组成。以下代码片段展示了这一点:

// ToString method
func (employee Employee) ToString() string {
    return fmt.Sprintf("%s: %d,%s,%d", employee.Name, employee.Age,employee.ID, 
    employee.SSN)
}

SortByAge类型

SortByAge方法根据Age对相关元素进行排序。SortByAge接口在Employee数组上操作。以下代码片段展示了这一点:

// SortByAge type
type SortByAge []Employee

// SortByAge interface methods
func (sortIntf SortByAge) Len() int { return len(sortIntf) }
func (sortIntf SortByAge) Swap(i int, j int) { sortIntf[i], sortIntf[j] = sortIntf[j], sortIntf[i] }
func (sortIntf SortByAge) Less(i int, j int) bool { return sortIntf[i].Age < sortIntf[j].Age }

main方法初始化员工数组并按年龄对数组进行排序:

func main() {
    var employees = []Employee{
        {"Graham","231",235643,31},
        {"John", "3434",245643,42},
        {"Michael","8934",32432, 17},
        {"Jenny", "24334",32444,26},
    }
    fmt.Println(employees)
    sort.Sort(SortByAge(employees))
    fmt.Println(employees)
    sort.Slice(employees, func(i int, j int) bool {
        return employees[i].Age > employees[j].Age
    })
    fmt.Println(employees)
}

执行以下命令以运行sort_slice.go代码片段:

go run sort_slice.go

这是输出结果:

图片

按照以下排序标准对有序列表进行排序。sort_keys.go代码片段展示了如何根据各种标准对事物进行排序,例如namemassdistanceMassMiles单位定义为float64

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and sort package
import (
    "fmt"
    "sort"
)

// Mass and Miles Types
type Mass float64
type Miles float64

下一个部分将讨论Thing结构定义。

Thing

以下代码定义了一个具有namemassdistancemeltingpointfreezingpoint属性的Thing类:

// Thing class
type Thing struct {
    name string
    mass Mass
    distance Miles
    meltingpoint int
    freezingpoint int
}

下一个部分将讨论ByFactor函数类型的实现。

ByFactor函数类型

ByFactor是一种less函数类型。以下代码片段展示了ByFactor类型:

// ByFactor function type
type ByFactor func(Thing1 *Thing, Thing2 *Thing) bool

Sort方法

Sort方法是一个具有byFactor参数的函数,如下所示:

// Sort method 
func (byFactor ByFactor) Sort(Things []Thing) {
    var sortedThings *ThingSorter
    sortedThings = &ThingSorter{
        Things: Things,
        byFactor: byFactor, 
    }
    sort.Sort(sortedThings)
}

Thing排序器类

Thing排序器根据其属性对元素进行排序。ThingSorter类有一个事物数组和一个byFactor方法:

// ThingSorter class
type ThingSorter struct {
    Things []Thing
    byFactor func(Thing1 *Thing, Thing2 *Thing) bool 
}

下一个部分将讨论lenswapless方法的实现。

lenswapless方法

sort.Interface具有lenswapless方法,如下所示:

// Len method
func (ThingSorter *ThingSorter) Len() int {
    return len(ThingSorter.Things)
}

// Swap method
func (ThingSorter *ThingSorter) Swap(i int, j int) {
    ThingSorter.Things[i], ThingSorter.Things[j] = ThingSorter.Things[j],    
    ThingSorter.Things[i]
}

// Less method
func (ThingSorter *ThingSorter) Less(i int, j int) bool {
    return ThingSorter.byFactor(&ThingSorter.Things[i], &ThingSorter.Things[j])
}

main方法

main方法创建事物并使用值初始化它们。此方法展示了按距离降序排序的按massdistancename排序的事物:

// Main method
func main() {
  var Things = []Thing{
    {"IronRod", 0.055, 0.4, 3000, -180},
    {"SteelChair", 0.815, 0.7, 4000, -209},
    {"CopperBowl", 1.0, 1.0, 60, -30},
    {"BrassPot", 0.107, 1.5, 10000, -456},
  }

  var name func(*Thing, *Thing) bool
  name = func(Thing1 *Thing, Thing2 *Thing) bool {
    return Thing1.name < Thing2.name
  }
  var mass func(*Thing, *Thing) bool
  mass = func(Thing1 *Thing, Thing2 *Thing) bool {
    return Thing1.mass < Thing2.mass
  }
  var distance func(*Thing, *Thing) bool
  distance = func(Thing1 *Thing, Thing2 *Thing) bool {
    return Thing1.distance < Thing2.distance
  }
  var decreasingDistance func(*Thing, *Thing) bool
  decreasingDistance = func(p1, p2 *Thing) bool {
    return distance(p2, p1)
  }

  ByFactor(name).Sort(Things)
  fmt.Println("By name:", Things)
  ByFactor(mass).Sort(Things)
  fmt.Println("By mass:", Things)
  ByFactor(distance).Sort(Things)
  fmt.Println("By distance:", Things)
  ByFactor(decreasingDistance).Sort(Things)
  fmt.Println("By decreasing distance:", Things)
}

执行以下命令以运行sort_keys.go文件:

go run sort_keys.go

这是输出结果:

图片

下一个部分将讨论struct数据结构。

struct类型

可以使用不同的多字段集对struct类型(类)进行排序。在sort_multi_keys.go代码中,我们展示了如何对struct类型进行排序。一个名为Commit的类由usernamelangnumlines属性组成。username是一个字符串,lang是一个字符串,numlines是一个整数。以下代码中,Commit类根据提交和行数进行排序:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and sort package
import (
  "fmt"
  "sort"
)

// A Commit is a record of code checking
type Commit struct {
  username string
  lang string
  numlines int
}

在下一节中,将讨论multiSorter类的实现。

multiSorter

multiSorter类包含提交和lessFunction数组属性。multiSorter类实现了Sort接口以对提交进行排序,如下面的代码所示:

type lessFunc func(p1 *Commit, p2 *Commit) bool
// multiSorter class
type multiSorter struct {
 Commits []Commit
 lessFunction    []lessFunc
}

下一个部分将讨论multiSorter类的不同方法。

The Sort method

在以下代码片段中,multiSorterSort方法通过调用sort.Sort并传递multiSorter参数来对Commits数组进行排序:

// Sort method
func (multiSorter *multiSorter) Sort(Commits []Commit) {
    multiSorter.Commits = Commits
    sort.Sort(multiSorter)
}

OrderBy 方法

OrderedBy方法接收less函数并返回multiSortermultisorter实例由less函数初始化,如下面的代码片段所示:

// OrderedBy method
func OrderedBy(lessFunction ...lessFunc) *multiSorter {
  return &multiSorter{
    lessFunction: lessFunction,
  }
}

The len method

multiSorter类的len方法返回Commits数组的长度。Commits数组是multiSorter的一个属性:

// Len method
func (multiSorter *multiSorter) Len() int {
 return len(multiSorter.Commits)
}

The Swap method

multiSorterSwap方法接收整数ij作为输入。此方法交换索引ij处的数组元素:

// Swap method
func (multiSorter *multiSorter) Swap(i int, j int) {
  multiSorter.Commits[i] = multiSorter.Commits[j] 
  multiSorter.Commits[j] = multiSorter.Commits[i]
}

less 方法

multiSorter类的Less方法接收整数ij,并比较索引i处的元素与索引j处的元素:

func (multiSorter *multiSorter) Less(i int, j int) bool {

  var p *Commit
  var q *Commit
  p = &multiSorter.Commits[i]
  q = &multiSorter.Commits[j]

  var k int
  for k = 0; k < len(multiSorter.lessFunction)-1; k++ {
    less := multiSorter.lessFunction[k]
    switch {
    case less(p, q):
      return true
    case less(q, p):
      return false
    }
  }
  return multiSorter.lessFunctionk
}

The main method

main方法创建一个Commit数组并用值初始化数组。创建了按userlanguagelines排序的函数。OrderedBy返回一个multiSorter,其sort方法被userlanguageincreasingLinesdecreasingLines调用:

//main method
func main() {
  var Commits = []Commit{
    {"james", "Javascript", 110},
    {"ritchie", "python", 250},
    {"fletcher", "Go", 300},
    {"ray", "Go", 400},
    {"john", "Go", 500},
    {"will", "Go", 600},
    {"dan", "C++", 500},
    {"sam", "Java", 650},
    {"hayvard", "Smalltalk", 180},
  }
  var user func(*Commit, *Commit) bool
  user = func(c1 *Commit, c2 *Commit) bool {
    return c1.username < c2.username
  }
  var language func(*Commit, *Commit) bool
  language = func(c1 *Commit, c2 *Commit) bool {
    return c1.lang < c2.lang
  }
  var increasingLines func(*Commit, *Commit) bool
  increasingLines = func(c1 *Commit, c2 *Commit) bool {
    return c1.numlines < c2.numlines
  }
  var decreasingLines func(*Commit, *Commit) bool
  decreasingLines = func(c1 *Commit, c2 *Commit) bool {
    return c1.numlines > c2.numlines // Note: > orders downwards.
  }
  OrderedBy(user).Sort(Commits)
  fmt.Println("By username:", Commits)
  OrderedBy(user, increasingLines).Sort(Commits)
  fmt.Println("By username,asc order", Commits)
  OrderedBy(user, decreasingLines).Sort(Commits)
  fmt.Println("By username,desc order", Commits)
  OrderedBy(language, increasingLines).Sort(Commits)
  fmt.Println("By lang,asc order", Commits)
  OrderedBy(language, decreasingLines, user).Sort(Commits)
  fmt.Println("By lang,desc order", Commits)
}

运行以下命令以执行sort_multi_keys.go文件:

go run sort_multi_keys.go

这是输出:

下一个部分将讨论 HTML 无序列表数据结构。

无序列表

一个无序列表被实现为一个链表。在一个无序列表中,不需要维护连续内存中项的相对位置。值将以随机方式放置。

无序列表在 HTML 5.0 中以<ul>标签开始。每个列表项用<li>标签编码。以下是一个示例:

<ul>
    <li> First book </li>
    <li> Second book </li>
    <li> Third book </li>
</ul>

以下是在 Golang 中的无序列表示例。Node类有一个属性和一个nextNode指针,如下面的代码所示。链表将有一组具有属性属性的节点。无序列表在名为unordered_list.go的脚本中呈现:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package
import (
    "fmt"
)

//Node class
type Node struct {
    property int
    nextNode *Node
}

下一个部分将讨论UnOrderedList类的实现。

UnOrderedList 类

无序列表由未按数字排序的元素组成。一个UnOrderedList类有一个headNode指针作为属性。从头节点遍历到下一个节点,你可以遍历链表:

// UnOrderedList class
type UnOrderedList struct {
    headNode *Node
}

下一个部分将讨论UnOrderedList结构体的AddtoHead方法和IterateList方法。

AddtoHead 方法

AddtoHead方法将节点添加到无序列表的头部。UnOrderedList类的AddToHead方法有一个整数类型的属性参数。它将使headNode指向使用property创建的新节点,而nextNode指向无序列表的当前headNode

//AddToHead method of UnOrderedList class
func (UnOrderedList *UnOrderedList) AddToHead(property int) {
  var node = &Node{}
  node.property = property
  node.nextNode = nil
  if UnOrderedList.headNode != nil {
    node.nextNode = UnOrderedList.headNode
  }
  UnOrderedList.headNode = node
}

IterateList方法

UnOrderedList类的IterateList方法打印列表中节点的属性。以下代码展示了这一点:

//IterateList method iterates over UnOrderedList
func (UnOrderedList *UnOrderedList) IterateList() {
  var node *Node
  for node = UnOrderedList.headNode; node != nil; node = node.nextNode {
    fmt.Println(node.property)
  }
}

主方法

main方法创建了一个链表实例,并将整数属性1357添加到链表的头部。在添加元素后,打印链表的headNode属性:

// main method
func main() {
  var unOrderedList UnOrderedList
  unOrderedList = UnOrderedList{}
  unOrderedList.AddToHead(1)
  unOrderedList.AddToHead(3)
  unOrderedList.AddToHead(5)
  unOrderedList.AddToHead(7)
  unOrderedList.IterateList()
}

运行以下命令以执行代码包中的unordered_list.go文件:

go run unordered_list.go

这是输出:

图片

摘要

本章通过代码示例介绍了异构数据结构,如有序列表和无序列表。在有序列表部分,介绍了通过单个键、多个键和sort.Slice对切片进行排序。切片通过使结构体元素的数组实现sort.Sort接口来进行排序。无序列表被描述为具有无序值的链表。

下一章将介绍动态数据结构,如字典TreeSets序列同步 TreeSets可变 TreeSets

问题

  1. sort.Sort接口的哪个方法返回要排序的元素的大小?

  2. 需要将哪个函数传递给sort.Slice方法以对切片进行排序?

  3. swap方法对索引*i**j*处的元素做了什么?

  4. 使用sort.Sort对元素进行排序的默认顺序是什么?

  5. 你如何使用sort.Slice实现升序和降序排序?

  6. 你如何对一个数组进行排序并保持元素的原始顺序?

  7. 哪个接口用于反转数据的顺序?

  8. 展示一个对切片进行排序的示例。

  9. 哪个方法用于向无序列表添加元素?

  10. 编写一个浮点数无序列表的代码示例。

进一步阅读

如果你想了解更多关于异构数据结构的信息,以下书籍推荐:

  • 《设计模式》,作者:Erich Gamma, Richard Helm, Ralph Johnson, 和 John Vlissides

  • 《算法导论(第三版)》,作者:Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein

  • 《数据结构与算法:简易入门》,作者:Rudolph Russell

第七章:动态数据结构

动态数据结构是内存中一组元素,它具有扩展或收缩的适应性。这种能力使软件工程师能够精确控制使用的内存量。动态数据结构用于在键值存储中处理通用数据。它们可用于分布式缓存和存储管理。在需要动态添加或删除元素的情况下,动态数据结构非常有价值。它们的容量与较小的关系数据库或内存数据库相当。这些数据结构用于市场营销和客户关系管理应用程序。字典、TreeSet 和序列是动态数据结构的例子。

在本章中,我们将解释字典、TreeSet 和序列是什么,并通过代码示例向您展示它们的实现方式。

本章涵盖了以下动态数据结构:

  • 字典

  • TreeSet:

    • 同步 TreeSet

    • 可变 TreeSet

  • 序列:

    • Farey

    • Fibonacci

    • Look-and-say

    • Thue–Morse

技术要求

golang.org/doc/install 为您的操作系统安装 Go 版本 1.10。

本章代码的 GitHub URL 如下所示:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter07

字典

字典是一组唯一的键值对集合。字典是一种广泛有用的数据结构,用于存储一组数据项。它有一个键,每个键都与一个单独的项相关联。当给定一个键时,字典将恢复与该键关联的项。这些键可以是任何类型:字符串、整数或对象。当我们需要排序一个列表时,可以通过其键检索元素值。在这个集合中允许添加、删除、修改和查找操作。字典类似于其他数据结构,如哈希、映射和 HashMap。键/值存储用于分布式缓存和内存数据库。数组与字典在数据访问方式上有所不同。集合具有唯一项,而字典可以有重复值。

字典数据结构在以下流中使用:

  • 电话簿

  • 网络中的路由表

  • 操作系统中的页面表

  • 编译器中的符号表

  • 生物学中的基因组图谱

以下代码展示了如何初始化和修改一个字典。在这个片段中,字典的键是 DictKey,并且是一个字符串:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
"fmt"
  "sync"
)

// DictKey type
type DictKey string

以下章节将讨论字典中的类型和方法。

DictVal 类型

字典将 DictKey 类型的 DictVal 值映射到:

// DictVal type
type DictVal string

字典类

以下代码中的字典是一个具有字典元素的类,其中 DictKey 是键,DictVal 是值。它有一个 sync.RWMutex 属性,lock

// Dictionary class
type Dictionary struct {
    elements map[DictKey]DictVal
    lock sync.RWMutex
}

在以下部分中讨论了 PutRemoveContainFindRestNumberofElementsGetKeysGetValuesMain 方法。

Put 方法

有一个 Put 方法,如下面的示例所示,它分别接受 DictKeyDictVal 类型的 keyvalue 参数。调用字典 lock 实例的 Lock 方法,并延迟 Unlock 方法。如果字典中有空的 map 元素,则使用 make 初始化元素。如果 map 元素不为空,则设置 keyvalue

// Put method
func (dict *Dictionary) Put(key DictKey, value DictVal) {
    dict.lock.Lock()
    defer dict.lock.Unlock()
    if dict.elements == nil {
        dict.elements = make(map[DictKey]DictVal)
    }
    dict.elements[key] = value
}

put 方法的示例输出如下。put 方法接受键 1 和值 1。map 使用 keyvalue 更新:

图片

Remove 方法

字典有一个 remove 方法,如下面的代码所示,它有一个 DictKey 类型的 key 参数。如果从映射中移除了与 Dictkey 关联的值,则此方法返回 bool 值:

// Remove method
func (dict *Dictionary) Remove(key DictKey) bool {
    dict.lock.Lock()
    defer dict.lock.Unlock()
    var exists bool
    _, exists = dict.elements[key]
    if exists {
        delete(dict.elements, key)
    }
    return exists
}

Contains 方法

在以下代码中,Contains 方法有一个输入参数 key,类型为 DictKey,如果 key 存在于字典中,则返回 bool

// Contains method
func (dict *Dictionary) Contains(key DictKey) bool {
    dict.lock.RLock()
    defer dict.lock.RUnlock()
    var exists bool
    _, exists = dict.elements[key]
    return exists
}

Find 方法

Find 方法接受 DictKey 类型的 key 参数,并返回与键关联的 DictVal 类型。以下代码片段解释了 Find 方法:

// Find method
func (dict *Dictionary) Find(key DictKey) DictVal {
    dict.lock.RLock()
    defer dict.lock.RUnlock()
    return dict.elements[key]
}

Reset 方法

Dictionary 类的 Reset 方法在以下代码片段中展示。调用字典 lock 实例的 Lock 方法,并延迟 Unlockelements 映射使用 DictKey 键和 DictVal 值的映射初始化:

// Reset method
func (dict *Dictionary) Reset() {
    dict.lock.Lock()
    defer dict.lock.Unlock()
    dict.elements = make(map[DictKey]DictVal)
}

NumberOfElements 方法

Dictionary 类的 NumberOfElements 方法返回 elements 映射的长度。lock 实例的 RLock 方法被调用。在返回长度之前,将 lock 实例的 RUnlock 方法延迟;这在上面的代码片段中显示:

// NumberOfElements method
func (dict *Dictionary) NumberOfElements() int {
    dict.lock.RLock()
    defer dict.lock.RUnlock()
    return len(dict.elements)
}

GetKeys 方法

Dictionary 类的 GetKeys 方法在以下代码片段中展示。该方法返回 DictKey 元素的数组。调用锁实例的 RLock 方法,并延迟 RUnlock 方法。通过遍历元素的映射返回字典键:

// GetKeys method
func (dict *Dictionary) GetKeys() []DictKey {
    dict.lock.RLock()
    defer dict.lock.RUnlock()
    var dictKeys []DictKey
    dictKeys = []DictKey{}
    var key DictKey
    for key = range dict.elements {
        dictKeys = append(dictKeys, key)
    }
    return dictKeys
}

GetValues 方法

Dictionary 类的 GetValues 方法返回 DictVal 元素的数组。在以下代码片段中,调用 lock 实例的 RLock 方法,并延迟 RUnlock 方法。在遍历元素的映射后,返回字典值的数组:

// GetValues method 
func (dict *Dictionary) GetValues() []DictVal {
    dict.lock.RLock()
    defer dict.lock.RUnlock()
    var dictValues []DictVal
    dictValues = []DictVal{}
    var key DictKey
    for key = range dict.elements {
        dictValues = append(dictValues, dict.elements[key])
    }
    return dictValues
}

主方法

以下代码展示了主方法,其中初始化并打印了字典:

// main method
func main() {
  var dict *Dictionary = &Dictionary{}
  dict.Put("1","1")
  dict.Put("2","2")
  dict.Put("3","3")
  dict.Put("4","4")
  fmt.Println(dict)
}

执行以下命令以运行 dictionary.go 文件:

go run dictionary.go

输出如下:

图片

让我们看一下以下部分中的 TreeSet 数据结构。

TreeSet

TreeSets 在市场营销和客户关系管理应用中使用。TreeSet 是一个具有唯一元素的二叉树集合。元素按自然顺序排序。在下面的代码片段中,展示了 TreeSet 的创建、插入、搜索和 stringify 操作。如果集合为空,TreeSet 只允许一个 null 值。元素按顺序存储。addremovecontains 函数在 TreeSets 上的成本为 log(n):

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// TreeSet class
type TreeSet struct {
  bst *BinarySearchTree
}

我们将在以下章节中讨论不同的 TreeSet 方法。

插入树节点方法

TreeSet 类的 InsertTreeNode 方法接受 treeNodes 变量参数,参数类型为 TreeNode。在下面的代码中,具有 keyvalue 的元素被插入到 TreeSet 的二叉搜索树中:

// InsertTreeNode method
func (treeset *TreeSet) InsertTreeNode(treeNodes ...TreeNode) {
  var treeNode TreeNode
  for _, treeNode = range treeNodes {
    treeset.bst.InsertElement(treeNode.key, treeNode.value)
  }
}

InsertTreeNode 方法的示例输出如下。InsertTreeNode 方法接受 treeNodes 作为参数。treeNodes 被插入到具有值为 8rootNode 中:

删除方法

TreeSet 类的 Delete 方法在下面的代码片段中展示。在此方法中,具有提供键的 treeNodes 被移除:

// Delete method
func (treeset *TreeSet) Delete(treeNodes ...TreeNode) {
  var treeNode TreeNode
  for _, treeNode = range treeNodes {
    treeset.bst.RemoveNode(treeNode.key)
  }
}

中序遍历树方法

BinarySearchTree 类的 InOrderTraverseTree 方法接受 function 作为参数。lock 实例的 RLock 方法被调用。lock 实例的 RUnlock 方法被延迟。InOrderTraverseTree 使用树的 rootNode 和函数作为参数被调用:

//InOrderTraverseTree method
func (tree *BinarySearchTree) InOrderTraverseTree(function func(int)) {
  tree.lock.RLock()
  defer tree.lock.RUnlock()
  inOrderTraverseTree(tree.rootNode, function)
}

中序遍历树方法

inOrderTraverseTree 方法从树的左侧遍历到节点根,然后到树的右侧。inOrderTraverseTree 方法接受 treeNodefunction 作为参数。该方法递归地调用 inOrderTraverseTree 方法,并分别使用 functionleftNoderightNode 进行单独调用。function 方法使用 treeNodevalue 被调用:

// inOrderTraverseTree method
func inOrderTraverseTree(treeNode *TreeNode, function func(int)) {
  if treeNode != nil {
    inOrderTraverseTree(treeNode.leftNode, function)
    function(treeNode.value)
    inOrderTraverseTree(treeNode.rightNode, function)
  }
}

预序遍历树方法

BinarySearchTree 类的 PreOrderTraverseTree 方法接受函数作为其参数。首先在树的 lock 实例上调用 Lock 方法,然后延迟调用 Unlock 方法。使用树的 rootNode 和函数作为参数调用 PreOrderTraverseTree 方法:

// PreOrderTraverse method
func (tree *BinarySearchTree) PreOrderTraverseTree(function func(int)) {
  tree.lock.Lock()
  defer tree.lock.Unlock()
  preOrderTraverseTree(tree.rootNode, function)
}

预序遍历树方法

preOrderTraverseTree 方法从根开始遍历树,到树的左侧和右侧。preOrderTraverseTree 方法接受 treeNodefunction 作为参数。如果 treeNode 不是 nil,则使用 treeNodevalue 调用 function,然后使用 functionleftNoderightNode 作为参数调用 preOrderTraverseTree 方法:

// preOrderTraverseTree method
func preOrderTraverseTree(treeNode *TreeNode, function func(int)) {
  if treeNode != nil {
    function(treeNode.value)
    preOrderTraverseTree(treeNode.leftNode, function)
    preOrderTraverseTree(treeNode.rightNode, function)
  }
}

搜索方法

TreeSet 类的 Search 方法接受一个名为 treeNodes 的变量参数,参数类型为 TreeNode。如果这些 treeNodes 中的任何一个存在,则返回 true;否则,返回 false。以下代码片段概述了 Search 方法:

// Search method
func (treeset *TreeSet) Search(treeNodes ...TreeNode) bool {
  var treeNode TreeNode
  var exists bool
  for _, treeNode = range treeNodes {
    if exists = treeset.bst.SearchNode(treeNode.key); !exists {
      return false
    }
  }
  return true
}

String 方法

在以下代码片段中,TreeSet 类的 String 方法返回 bst 的字符串版本:

// String method
func (treeset *TreeSet) String() {
  treeset.bst.String()
}

主方法

TreeSet 类中的 main 方法使用 TreeNodes 创建一个 TreeSet。以下代码片段创建了一个 TreeSet 并调用了 String 方法:

// main method
func main() {
  var treeset *TreeSet = &TreeSet{}
  treeset.bst = &BinarySearchTree{}
  var node1 TreeNode = TreeNode{8,8, nil,nil}
  var node2 TreeNode = TreeNode{3,3,nil, nil}
  var node3 TreeNode = TreeNode{10,10,nil,nil}
  var node4 TreeNode = TreeNode{1,1,nil,nil}
  var node5 TreeNode = TreeNode{6,6,nil,nil}
  treeset.InsertTreeNode(node1,node2,node3, node4, node5)
  treeset.String()
}

执行以下命令以运行 treeset.gobinarysearchtree.go 文件:

$ go build treeset.go binarysearchtree.go
$ ./treeset

输出如下:

图片

下一节将讨论同步的 TreeSet 数据结构。

同步 TreeSet

在同步 TreeSet 上执行的操作在多个调用之间同步,这些调用访问 TreeSet 的元素。TreeSet 中的同步是通过使用 sync.RWMutex 锁来实现的。在树的 lock 实例上调用 lock 方法,并在插入、删除或更新 tree 节点之前延迟调用解锁方法:

// InsertElement method
func (tree *BinarySearchTree) InsertElement(key int, value int) {
  tree.lock.Lock()
  defer tree.lock.Unlock()
  var treeNode *TreeNode
  treeNode = &TreeNode{key, value, nil, nil}
  if tree.rootNode == nil {
    tree.rootNode = treeNode
  } else {
    insertTreeNode(tree.rootNode, treeNode)
  }
}

可变 TreeSet

可变 TreeSet 可以在树及其节点上使用 addupdatedelete 操作。insertTreeNode 通过传递要更新的 rootNodetreeNode 参数来更新树。以下代码片段展示了如何使用给定的 rootNodeTreeNode 插入一个 TreeNode

// insertTreeNode method
func insertTreeNode(rootNode *TreeNode, newTreeNode *TreeNode) {
  if newTreeNode.key < rootNode.key {
    if rootNode.leftNode == nil {
      rootNode.leftNode = newTreeNode
    } else {
      insertTreeNode(rootNode.leftNode, newTreeNode)
    }
  } else {
    if rootNode.rightNode == nil {
      rootNode.rightNode = newTreeNode
    } else {
      insertTreeNode(rootNode.rightNode, newTreeNode)
    }
  }
}

让我们在接下来的几节中讨论不同的可变 TreeSet。

RemoveNode 方法

BinarySearchTreeRemoveNode 方法如下:

// RemoveNode method
func (tree *BinarySearchTree) RemoveNode(key int) {
  tree.lock.Lock()
  defer tree.lock.Unlock()
  removeNode(tree.rootNode, key)
}

Treeset.bst

可以通过访问 treeset.bst 并从 rootNode 及其左右节点遍历二叉搜索树来更新 TreeNode,如下所示:

  var treeset *TreeSet = &TreeSet{}
  treeset.bst = &BinarySearchTree{} 
  var node1 TreeNode = TreeNode{8, 8, nil, nil}
  var node2 TreeNode = TreeNode{3, 3, nil, nil}
  var node3 TreeNode = TreeNode{10, 10, nil, nil}
  var node4 TreeNode = TreeNode{1, 1, nil, nil}
  var node5 TreeNode = TreeNode{6, 6, nil, nil}
  treeset.InsertTreeNode(node1, node2, node3, node4, node5)
  treeset.String()

在下一节中,我们将查看序列。

序列

序列 是一组按特定顺序排列的数字。流中元素的数量可以是无限的,这些序列被称为 子序列 是从另一个序列创建的序列。在删除序列中的一些元素后,子序列中元素的相对位置将保持不变。

在接下来的几节中,我们将查看不同的序列,如 Farey 序列、Fibonacci 序列、look-and-say 和 Thue–Morse。

Farey 序列

Farey 序列 由介于零和一之间的简化分数组成。分数的分母小于或等于 m,并按升序排列。这个序列被称为 Farey 系列。在以下代码中,显示了简化分数:

///main package has examples shown
// in Go Data Structures and algorithms book
package main
// importing fmt package
import (
  "fmt"
)

// fraction class
type fraction struct {
  numerator int
  denominator int
}

让我们看看 Farey 序列中的不同方法。

String 方法

fraction 类具有分子和分母的整数属性。fraction 类的 String 方法,如以下代码片段所示,返回 fraction 的字符串版本:

// string method of fraction class
func (frac fraction) String() string {
  return fmt.Sprintf("%d/%d", frac.numerator, frac.denominator)
}

g 方法

g 方法接受两个分数并打印出一系列简化的分数。g 函数接受 lr 分数以及 num 整数作为参数,以打印简化的分数作为一系列。以下代码片段展示了 g 方法:

// g method
func g(l fraction, r fraction, num int) {
  var frac fraction
  frac = fraction{l.numerator + r.numerator, l.denominator + r.denominator}
  if frac.denominator <= num {
    g(l, frac, num)
    fmt.Print(frac, " ")
    g(frac, r, num)
  }
}

主方法

以下代码片段展示了 main 方法。在 main 方法中,使用递归打印简化的分数序列:

// main method
func main() {
 var num int
 var l fraction
 var r fraction
 for num = 1; num <= 11; num++ {
 l = fraction{0, 1}
 r = fraction{1, 1}
 fmt.Printf("F(%d): %s ", num, l)
 g(l, r, num)
 fmt.Println(r)
 }

运行以下命令以执行 farey_sequence.go 文件:

go run farey_sequence.go

输出如下:

图片

下一节将讨论斐波那契数列数据结构。

斐波那契数列

斐波那契数列由一系列数字组成,其中每个数字都是前两个数字的和。Pingala 在公元前 200 年首次提出了斐波那契数。斐波那契数列如下:

图片

斐波那契数列的递推关系如下:

图片

种子值如下:

图片

一个斐波那契素数是一个素数的斐波那契数。斐波那契素数序列如下:

图片

计算机算法,如斐波那契搜索技术、堆和立方体,是斐波那契数的流行应用。伪随机数生成器使用斐波那契数。

以下代码片段展示了斐波那契数列及其递归斐波那契数计算。同时展示了 Series 函数。Series 函数用于计算数列中的斐波那契数:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and strconv package
import (
  "fmt"
  "strconv"
)

// Series method
func Series(n int) int {
  var f []int
  f = make([]int, n+1, n+2)
  if n < 2 {
    f = f[0:2]
  }
  f[0] = 0
  f[1] = 1
  var i int
  for i = 2; i <= n; i++ {
    f[i] = f[i-1] + f[i-2]
  }
  return f[n]
}

下一节将讨论斐波那契数列的不同方法。

FibonacciNumber 方法

FibonacciNumber 方法接受一个整数 n,并通过递归计算斐波那契数。以下代码片段展示了这种递归:

// FibonacciNumber method
func FibonacciNumber(n int) int {
  if n <= 1 {
    return n
  }
  return FibonacciNumber(n-1) + FibonacciNumber(n-2)
}

主方法

以下代码片段中的 main 方法展示了斐波那契数列的计算方法:

// main method
func main() {
 var i int
 for i = 0; i <= 9; i++ {
 fmt.Print(strconv.Itoa(Series(i)) + " ")
 }
 fmt.Println("")
 for i = 0; i <= 9; i++ {
 fmt.Print(strconv.Itoa(FibonacciNumber(i)) + " ")
 }
 fmt.Println("")
}

运行以下命令以执行 fibonacci_sequence.go 文件:

go run fibonacci_sequence.go

输出如下:

图片

下一节将讨论观察法数据结构。

观察法

观察法序列是一个整数序列:

图片

该序列是通过计算组中前一个数的数字生成的。John Conway 最初提出了 观察法序列 这个术语。

观察法序列在以下代码中展示。look_say 方法接受一个字符串作为参数,并返回一个观察法整数序列:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and strconv package
import (
  "fmt"
  "strconv"
)

// look_say method
func look_say(str string) (rstr string) {
  var cbyte byte
  cbyte = str[0]
  var inc int
  inc = 1
  var i int
  for i = 1; i < len(str); i++ {
    var dbyte byte
    dbyte = str[i]
    if dbyte == cbyte {
      inc++
      continue
    }
    rstr = rstr + strconv.Itoa(inc) + string(cbyte)
    cbyte = dbyte
    inc = 1
  }
  return rstr + strconv.Itoa(inc) + string(cbyte)
}

main 方法初始化字符串并调用 look_say 方法。该方法返回的观察法序列被打印出来:

// main method
func main() {
  var str string
  str = "1"
  fmt.Println(str)
  var i int
  for i = 0; i < 8; i++ {
    str = look_say(str)
    fmt.Println(str)
  }
}

运行以下命令以执行 look_say.go 文件:

go run look_say.go

输出如下:

图片

下一节将讨论 Thue–Morse 数据结构。

Thue–Morse

Thue–Morse 序列是一个从零开始的二进制序列,它附加了当前序列的布尔补码。

Thue–Morse 序列如下:

图片

Thue–Morse 序列被 Eugene Prophet 应用,并被 Axel Thue 用于研究词的组合数学。Thue–Morse 序列在分形曲线领域得到应用,例如 Koch 雪花。

以下代码片段创建 Thue–Morse 序列。ThueMorseSequence函数接收一个bytes.Buffer实例 buffer,并通过在bytes上应用complement操作来修改 buffer 为 Thue–Morse 序列:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
 "bytes"
 "fmt"
)

// ThueMorseSequence method
func ThueMorseSequence(buffer *bytes.Buffer) {

 var b int
 var currLength int
 var currBytes []byte
 for b, currLength, currBytes = 0, buffer.Len(), buffer.Bytes(); b < currLength; b++ {
 if currBytes[b] == '1' {
 buffer.WriteByte('0')
 } else {
 buffer.WriteByte('1')
 }
 }
}

main方法初始化序列号为0ThueMorseSequence方法接收bytes.Buffer的指针,并通过调用ThueMorseSequence方法来修改它。结果序列在终端上打印:

// main method
func main() {
 var buffer bytes.Buffer
 // initial sequence member is "0"
 buffer.WriteByte('0')
 fmt.Println(buffer.String())
 var i int
 for i = 2; i <= 7; i++ {
 ThueMorseSequence(&buffer)
 fmt.Println(buffer.String())
 }
}

运行以下命令以执行thue_morse.go文件:

go run thue_morse.go

输出如下:

图片

摘要

本章介绍了字典数据结构的containsputremovefindresetNumberOfElementsgetKeysgetValues方法。InsertTreeNodeDeleteSearchstringify TreeSet 操作已详细解释,并提供了代码示例。代码中展示了BinarySearchTree结构,以及InsertElementInOrderTraversalPreOrderTraverseTreeSearchNodeRemoveNode函数。

下一章涵盖了排序、搜索、递归和散列等算法。

问题

  1. 如何确保BinarySearchTree同步?

  2. 哪个方法被调用以推迟函数的调用?

  3. 如何使用自定义类型定义字典的键和值?

  4. 如何找到映射的长度?

  5. 在树中遍历treeNodes列表时使用什么关键字?

  6. 在费雷序列中,序列中的实数被称为什么?

  7. 斐波那契数是什么?

  8. 如何将整数转换为字符串?

  9. 用于将字节转换为字符串的方法是什么?

  10. 向字典中添加元素时调用什么方法?

进一步阅读

如果你想了解更多关于动态数据结构的信息,以下书籍推荐:

  • 设计模式》,作者 Erich Gamma, Richard Helm, Ralph Johnson, 和 John Vlissides

  • 算法导论(第三版)》,作者 Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein

  • 数据结构与算法:简易入门》,作者 Rudolph Russell

第八章:经典算法

经典算法在数据搜索和密码学领域得到应用。排序、搜索、递归和哈希算法是经典算法的好例子。排序算法用于将元素按升序或降序排列。这些算法通常用于规范化数据和创建可读内容。搜索算法用于在集合中查找元素。递归算法是一种调用自身输入项的算法。哈希算法是一种加密哈希技术。它是一种将具有主观大小的数据映射到具有固定大小的哈希的科学计算。它旨在是一个单向函数,你不能改变它。

在本章中,我们将介绍不同的经典算法,并通过合适的示例进行解释。

本章涵盖了以下算法:

  • 排序:

    • 冒泡

    • 选择

    • 插入

    • 希尔

    • 归并

    • 快速

  • 搜索:

    • 线性

    • 顺序

    • 二进制

    • 插值

  • 递归

  • 哈希

技术要求

为您的操作系统安装 Go 版本 1.10,请访问golang.org/doc/install

本章中代码的 GitHub URL 如下:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter08

排序

排序算法将集合中的元素按升序或降序排列。字典序可以应用于字符和字符串的集合。这些算法的效率在于将输入数据排序成有序集合的性能。最佳排序算法的时间复杂度是O(n log n)。排序算法根据以下标准进行分类:

  • 计算复杂度

  • 内存使用

  • 稳定性

  • 排序类型:串行/并行

  • 适应性

  • 排序方法

在以下章节中,我们将探讨不同的排序算法,即冒泡、选择、插入、希尔、归并和快速排序。

气泡

冒泡排序算法是一种排序算法,它比较相邻的两个元素,如果它们顺序错误则交换它们。该算法的复杂度为O(n²),其中 n 是要排序的元素数量。最小或最大的值会冒泡到集合的顶部,或者最小或最大的值会沉到集合的底部(取决于你是按升序还是降序排序)。

以下代码片段展示了冒泡排序算法的实现。bubbleSorter函数接收一个整数数组,并按升序对数组元素进行排序。

main方法初始化数组整数并调用bubbleSorter函数,如下所示:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
  "fmt"
)

//bubble Sorter method
func bubbleSorter(integers [11]int) {

  var num int
  num = 11
  var isSwapped bool
  isSwapped = true
  for isSwapped {
    isSwapped = false
    var i int
    for i = 1; i < num; i++ {
      if integers[i-1] > integers[i] {

        var temp = integers[i]
        integers[i] = integers[i-1]
        integers[i-1] = temp
        isSwapped = true
      }
    }
  }
  fmt.Println(integers)
}

// main method
func main() {
  var integers [11]int = [11]int{31, 13, 12, 4, 18, 16, 7, 2, 3, 0, 10}
  fmt.Println("Bubble Sorter")
  bubbleSorter(integers)

}

执行以下命令以运行bubble_sort.go文件:

go run bubble_sort.go

输出如下:

图片

让我们在下一节中看看选择排序算法。

选择

选择排序是一种将输入集合分成两个片段的算法。通过从列表的左侧交换最小或最大的元素到右侧,对这个元素子列表进行排序。该算法的时间复杂度为O(n²)。对于大型集合,此算法效率低下,性能不如插入排序算法。

以下代码显示了SelectionSorter函数的实现,该函数接受要排序的集合:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

// Selection Sorter method
func SelectionSorter(elements []int) {

  var i int
  for i = 0; i < len(elements)-1; i++ {
    var min int
    min = i
    var j int
    for j = i + 1; j <= len(elements)-1; j++ {
      if elements[j] < elements[min] {
        min = j
      }
    }
    swap(elements, i, min)
  }
}

让我们在下一节中看看不同的选择方法。

交换方法

swap方法接受元素数组以及ij索引作为参数。该方法将位置i的元素与位置j的元素进行交换,如下所示:

// swap method
func swap(elements []int, i int, j int) {
  var temp int
  temp = elements[j]
  elements[j] = elements[i]
  elements[i] = temp
}

main方法

main方法初始化elements数组。在以下代码片段中,elements在排序前后被打印出来:

//main method
func main() {
  var elements []int
  elements = []int{11, 4, 18, 6, 19, 21, 71, 13, 15, 2}
  fmt.Println("Before Sorting ", elements)
  SelectionSorter(elements)
  fmt.Println("After Sorting", elements)
}

运行以下命令以执行selection_sort.go文件:

go run selection_sort.go

输出如下:

让我们在下一节中看看插入排序算法。

插入

插入排序是一种一次创建一个最终排序数组的算法。该算法的性能时间复杂度为O(n²)。与其他算法(如快速排序、堆排序和归并排序)相比,在大型集合上效率较低。在现实生活中,桥牌游戏中玩家手动排序牌的例子是插入排序的一个很好的例子。

希尔排序算法的实现如下所示。RandomSequence函数接受元素数量作为参数并返回一个随机整数数组:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
  "fmt"
  "math/rand"
  "time"
)

// randomSequence method
func randomSequence(num int) []int {

    var sequence []int
    sequence = make([]int, num,num)
    rand.Seed(time.Now().UnixNano())
    var i int
    for i= 0; i < num; i++ {
        sequence[i] = rand.Intn(999) - rand.Intn(999)
    }
    return sequence
}

让我们在下一节中看看不同的插入方法。

InsertionSorter 方法

InsertionSorter方法的实现如下所示。此方法接受整数数组作为参数并对其进行排序:

//InsertionSorter method
func InsertionSorter(elements []int) {
    var n = len(elements)
    var i int

    for i = 1; i < n; i++ {
        var j int
        j = i
        for j > 0 {
            if elements[j-1] > elements[j] {
                elements[j-1], elements[j] = elements[j], elements[j-1]
            }
            j = j - 1
        }
    }
}

main方法

main方法通过调用randomSequence函数初始化sequence,如下面的代码所示。InsertionSorter函数接受sequence并按升序对其进行排序:

//main method
func main() {

    var sequence []int
    sequence = randomSequence(24)
    fmt.Println("\n^^^^^^ Before Sorting ^^^ \n\n", sequence)
    InsertionSorter(sequence)
    fmt.Println("\n--- After Sorting ---\n\n", sequence, "\n")
}

运行以下命令以执行insertion_sort.go文件:

go run insertion_sort.go

输出如下:

让我们在下一节中看看希尔排序算法。

希尔

希尔排序算法对集合中顺序不正确的元素对进行排序。要比较的元素之间的距离依次减小。此算法比快速排序算法执行更多操作,具有更高的缓存未命中率。

在下面的代码中,我们可以看到希尔排序算法的实现。ShellSorter函数接受一个整数数组作为参数并对其进行排序:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
  "fmt"
)

// shell sorter method
func ShellSorter(elements []int) {
  var (
    n = len(elements)
    intervals = []int{1}
    k = 1

  )

  for {
    var interval int
    interval = power(2, k) + 1
    if interval > n-1 {
      break
    }
    intervals = append([]int{interval}, intervals...)
    k++
  }
  var interval int
  for _, interval = range intervals {
    var i int
    for i = interval; i < n; i += interval {
      var j int
      j = i
      for j > 0 {
        if elements[j-interval] > elements[j] {
          elements[j-interval], elements[j] = elements[j], elements[j-interval]
        }
        j = j - interval
      }
    }
  }
}

让我们在下一节中看看不同的希尔方法。

功率方法

power方法接受exponentindex作为参数,并返回指数的指数次幂,如下所示:

//power function
func power(exponent int, index int) int {
  var power int
  power = 1
  for index > 0 {
    if index&1 != 0 {
      power *= exponent
    }
    index >>= 1
    exponent *= exponent
  }
  return power
}

main方法

main方法初始化elements整数数组并调用ShellSorter方法,如下所示:

// main method
func main() {
  var elements []int
  elements = []int{34, 202, 13, 19, 6, 5, 1, 43, 506, 12, 20, 28, 17, 100, 25, 4, 5, 97, 1000, 27}
  ShellSorter(elements)
  fmt.Println(elements)
}

运行以下命令以执行shell_sort.go文件:

go run shell_sort.go

输出如下:

让我们看一下下一节中的归并排序算法。

归并

归并排序算法是一种基于比较的方法,由约翰·冯·诺伊曼发明。相邻列表中的每个元素都会进行比较以进行排序。算法的性能是O(n log n)。此算法是排序链表的最佳算法。

以下代码片段演示了归并排序算法。createArray函数接受num int作为参数,并返回一个由随机元素组成的整数array

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
  "fmt"
  "math/rand"
  "time"
)

// create array
func createArray(num int) []int {
  var array []int
  array = make([]int, num, num)
  rand.Seed(time.Now().UnixNano())
  var i int
  for i = 0; i < num; i++ {
    array[i] = rand.Intn(99999) - rand.Intn(99999)
  }
  return array
}

让我们看一下以下章节中不同的归并方法。

MergeSorter方法

MergeSorter方法接受一个整数元素数组作为参数,并将两个元素子数组递归地传递给MergeSorter方法。结果数组被连接并返回作为集合,如下所示:

// MergeSorter algorithm
func MergeSorter(array []int) []int {

  if len(array) < 2 {
    return array
  }
  var middle int
  middle = (len(array)) / 2
  return JoinArrays(MergeSorter(array[:middle]), MergeSorter(array[middle:]))
}

JoinArrays方法

JoinArrays函数接受leftArrrightArr整数数组作为参数。以下代码返回合并后的数组:

// Join Arrays method
func JoinArrays(leftArr []int, rightArr []int) []int {

  var num int
  var i int
  var j int
  num, i, j = len(leftArr)+len(rightArr), 0, 0
  var array []int
  array = make([]int, num, num)

  var k int
  for k = 0; k < num; k++ {
    if i > len(leftArr)-1 && j <= len(rightArr)-1 {
      array[k] = rightArr[j]
      j++
    } else if j > len(rightArr)-1 && i <= len(leftArr)-1 {
      array[k] = leftArr[i]
      i++
    } else if leftArr[i] < rightArr[j] {
      array[k] = leftArr[i]
      i++
    } else {
      array[k] = rightArr[j]
      j++
    }
  }
  return array
}

main方法

main方法初始化一个包含40个元素的整数数组,并在排序前后打印元素,如下所示:

// main method
func main() {

  var elements []int
  elements = createArray(40)
  fmt.Println("\n Before Sorting \n\n", elements)
  fmt.Println("\n-After Sorting\n\n", MergeSorter(elements), "\n")
}

运行以下命令以执行merge_sort.go文件:

go run merge_sort.go

输出如下:

让我们看一下以下章节中的快速排序算法。

快速

快速排序是一种对集合中的元素进行有序排序的算法。并行化后的快速排序比归并排序和堆排序快两到三倍。算法的性能是O(n log n)。此算法是二叉树排序算法的空间优化版本。

在以下代码片段中,实现了快速排序算法。QuickSorter函数接受一个整数elements数组,以及upper intbelow int作为参数。函数将数组分成部分,这些部分被递归地分割和排序:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

//Quick Sorter method
func QuickSorter(elements []int, below int, upper int) {
  if below < upper {
    var part int
    part = divideParts(elements, below, upper)
    QuickSorter(elements, below, part-1)
    QuickSorter(elements, part+1, upper)
  }
}

让我们看一下以下章节中不同的快速方法。

divideParts方法

divideParts方法接受一个整数elements数组,upper intbelow int作为参数。该方法按升序排序元素,如下所示:

// divideParts method
func divideParts(elements []int, below int, upper int) int {
  var center int
  center = elements[upper]
  var i int
  i = below
  var j int
  for j = below; j < upper; j++ {
    if elements[j] <= center {
      swap(&elements[i], &elements[j])
      i += 1
    }
  }
  swap(&elements[i], &elements[upper])
  return i
}

交换方法

在以下代码片段中,swap方法通过交换值来交换元素:

//swap method
func swap(element1 *int, element2 *int) {
  var val int
  val = *element1
  *element1 = *element2
  *element2 = val
}

main方法

main方法要求用户输入元素的个数和要read的元素。在排序前后,array被初始化并打印,如下所示:

// main method
func main() {
  var num int

  fmt.Print("Enter Number of Elements: ")
  fmt.Scan(&num)

  var array = make([]int, num)

  var i int
  for i = 0; i < num; i++ {
    fmt.Print("array[", i, "]: ")
    fmt.Scan(&array[i])
  }

  fmt.Print("Elements: ", array, "\n")
  QuickSorter(array, 0, num-1)
  fmt.Print("Sorted Elements: ", array, "\n")
}

执行以下命令以运行quick_sort.go文件:

go run quick_sort.go

输出如下:

现在我们已经完成了排序算法,让我们在下一节中看看搜索算法。

搜索

搜索算法用于检索存储在数据源或集合中的信息。算法被赋予待查元素的键,并将找到相关值。搜索算法根据信息的可用性返回布尔值 true 或 false。它们可以被增强以显示与搜索标准相关的多个值。不同的搜索算法类型包括线性、二分和插值。这些算法根据搜索类型进行分类。搜索算法包括暴力法和启发式方法。算法的选择基于其效率。选择这些算法的不同因素如下:

  • 输入类型

  • 输出类型

  • 明确性

  • 正确性

  • 完整性

  • 有效性

  • 通用性

在本节中,我们将讨论不同类型的搜索算法。

线性

线性搜索方法通过依次检查集合中的每个元素来在集合中查找给定的值。线性搜索算法的时间复杂度是O(n)。二分搜索算法和哈希表的性能优于此搜索算法。

以下代码片段展示了线性搜索方法的实现。LinearSearch函数接受一个整数数组elementsfindElement int作为参数。如果找到findElement,函数返回布尔值true;否则,返回false

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

// Linear Search method
func LinearSearch(elements []int, findElement int) bool {
  var element int
  for _, element = range elements {
    if element == findElement {
      return true
    }
  }
  return false
}

main方法初始化整数数组elements,并通过传递需要找到的整数调用LinearSearch方法,如下所示:

// main method
func main() {
  var elements []int
  elements = []int{15, 48, 26, 18, 41, 86, 29, 51, 20}
  fmt.Println(LinearSearch(elements, 48))
}

执行以下命令以运行linear_search.go文件:

go run linear_search.go

输出如下:

让我们看一下以下章节中的二分搜索算法。

二分

二分搜索算法将输入值与排序集合的中间元素进行比较。如果不相等,则消除未找到元素的半部分。搜索继续在集合的剩余半部分进行。此算法的时间复杂度为O(log n)。

以下代码片段展示了使用sort包中的sort.Search函数实现的二分搜索算法。main方法初始化elements数组,并调用sort.Search函数以查找整数元素:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
  "sort"
)

// main method
func main() {
  var elements []int
  elements = []int{1, 3, 16, 10, 45, 31, 28, 36, 45, 75}
  var element int
  element = 36

  var i int

  i = sort.Search(len(elements), func(i int) bool { return elements[i] >= element })
  if i < len(elements) && elements[i] == element {
    fmt.Printf("found element %d at index %d in %v\n", element, i, elements)
  } else {
    fmt.Printf("element %d not found in %v\n", element, elements)
  }
}

执行以下命令以运行binary_search.go文件:

go run binary_search.go

输出如下:

让我们看一下以下章节中的插值搜索算法。

插值

插值搜索算法在有序集合中搜索元素。该算法通过在估计位置之前或之后减小搜索空间来找到输入元素。搜索算法的时间复杂度为 O(log log n)。

以下代码片段实现了插值搜索算法。InterpolationSearch 函数接受整数元素数组和要查找的整数元素作为参数。该函数在集合中找到元素,并返回找到的元素的布尔值和索引:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

//interpolation search method
func InterpolationSearch(elements []int, element int) (bool, int) {
  var mid int
  var low int
  low = 0
  var high int
  high = len(elements) - 1

  for elements[low] < element && elements[high] > element {
    mid = low + ((element-elements[low])*(high-low))/(elements[high]-elements[low])

    if elements[mid] < element {
      low = mid + 1
    } else if elements[mid] > element {
      high = mid - 1
    } else {
      return true, mid
    }
  }

  if elements[low] == element {
    return true, low
  } else if elements[high] == element {
    return true, high
  } else {
    return false, -1
  }

  return false, -1
}

main 方法初始化整数元素数组,并使用 elements 数组和 element 参数调用 InterpolationSearch 方法,如下所示:

// main method
func main() {
  var elements []int
  elements = []int{2, 3, 5, 7, 9}
  var element int
  element = 7
  var found bool
  var index int
  found, index = InterpolationSearch(elements, element)
  fmt.Println(found, "found at", index)
}

运行以下命令以执行 interpolation_search.go 文件:

go run interpolation_search.go

输出如下:

图片

现在我们已经完成了搜索算法,接下来让我们看看下一节中的递归算法。

递归

递归是一种算法,其中一个步骤会调用当前正在运行的方法或函数。该算法通过应用基本任务并返回值来获取输入的结果。这种方法在第一章,数据结构与算法分而治之算法部分中简要讨论过。在递归过程中,如果未达到基本条件,则可能会出现栈溢出条件。

以下代码片段实现了递归算法。Factor 方法将 num 作为参数,并返回 num 的阶乘。该方法使用递归来计算数字的阶乘:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and bytes package
import (
  "fmt"
)

//factorial method
func Factor(num int) int {
  if num <= 1 {
    return 1
  }
  return num * Factor(num-1)
}

main 方法定义了一个值为 12 的整数并调用 Factor 方法。打印出数字 12 的阶乘,如下所示:

//main method
func main() {
  var num int = 12
  fmt.Println("Factorial: %d is %d", num, Factor(num))
}

运行以下命令以执行 recurse_factorial.go 文件:

go run recurse_factorial.go

输出如下:

图片

现在我们已经完成了递归算法,接下来让我们看看下一节中的哈希算法。

哈希

在第四章,非线性数据结构中介绍了哈希函数。Go 中的哈希实现有 crc32sha256 实现。以下代码片段展示了使用 XOR 变换实现的具有多个值的哈希算法。CreateHash 函数接受一个 byte 数组,byteStr,作为参数,并返回该字节数组的 sha256 校验和:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
  "crypto/sha1"
  "hash"
)

//CreateHash method
func CreateHash(byteStr []byte) []byte {
  var hashVal hash.Hash
  hashVal = sha1.New()
  hashVal.Write(byteStr)

  var bytes []byte

  bytes = hashVal.Sum(nil)
  return bytes
}

在以下章节中,我们将讨论哈希算法的不同方法。

The CreateHashMutliple method

CreateHashMutliple 方法接受 byteStr1byteStr2 字节数组作为参数,并返回 XOR 变换后的字节数值,如下所示:

// Create hash for Multiple Values method
func CreateHashMultiple(byteStr1 []byte, byteStr2 []byte) []byte {
  return xor(CreateHash(byteStr1), CreateHash(byteStr2))
}

XOR 方法

xor方法接受byteStr1byteStr2字节数组作为参数,并返回 XOR 变换结果,如下所示:

// XOR method
func xor(byteStr1 []byte, byteStr2 []byte) []byte {
  var xorbytes []byte
  xorbytes = make([]byte, len(byteStr1))
  var i int
  for i = 0; i < len(byteStr1); i++ {
    xorbytes[i] = byteStr1[i] ^ byteStr2[i]
  }
  return xorbytes
}

主要方法

main方法调用createHashMutliple方法,传递CheckHash作为字符串参数,并打印字符串的哈希值,如下所示:

// main method
func main() {

  var bytes []byte
  bytes = CreateHashMultiple([]byte("Check"), []byte("Hash"))

  fmt.Printf("%x\n", bytes)
}

运行以下命令来执行hash.go文件:

go run hash.go

输出如下:

图片

摘要

本章介绍了冒泡排序、选择排序、插入排序、希尔排序、归并排序和快速排序等排序算法。讨论了线性搜索、二分搜索和插值搜索等搜索算法。最后,通过代码片段解释了递归和哈希算法。所有算法都伴随着代码示例和性能分析。

在下一章中,将介绍使用图表示网络和使用列表表示稀疏矩阵,以及相应的示例。

问题

  1. 冒泡排序的复杂度顺序是什么?

  2. 哪种排序算法一次取一个元素来创建一个最终排序的集合?

  3. 哪种排序方法可以排序彼此距离较远的元素对?

  4. 使用归并排序算法的复杂度是多少?

  5. 哪个算法更好:快速排序、归并排序还是堆排序算法?

  6. 有哪些不同类型的搜索算法?

  7. 提供一个递归算法的代码示例。

  8. 谁是第一个描述插值搜索的人?

  9. 哪种排序算法是基于相邻元素列表的比较方法?

  10. 谁发表了希尔排序算法?

进一步阅读

如果你想了解更多关于排序、选择、搜索和哈希等算法的信息,以下书籍推荐:

  • 《设计模式》,作者:艾里克·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利斯

  • 《算法导论 第 3 版》,作者:托马斯·H·科门、查尔斯·E·莱伊森、罗纳德·L·里维斯和克利福德·斯坦

  • 《数据结构和算法:简单入门》,作者:鲁道夫·拉塞尔

第三部分:使用 Go 的高级数据结构和算法

本节介绍了网络表示、稀疏矩阵表示、内存管理、基于实例的学习、编译器翻译和与进程调度相关的数据结构和算法。算法中展示的数据结构包括图、列表的列表、AVL 树、K-D 树、球树、Van Emde Boas 树、缓冲树和红黑树。通过代码示例和效率分析,涵盖了无缓存感知数据结构和数据流分析。

本节包含以下章节:

  • 第九章,网络和稀疏矩阵表示

  • 第十章,内存管理

第九章:网络和稀疏矩阵表示

稀疏矩阵是一种大部分值都是零的矩阵。零值与非零值的比率称为稀疏度。在创建关于网络可用性的假设时,对矩阵稀疏度的估计可能是有帮助的。在机器学习和自然语言解析中,广泛使用大量的稀疏矩阵。处理它们在计算上代价高昂。推荐引擎使用它们来表示目录内的产品。计算机视觉在处理包含暗像素区域的图片时使用稀疏矩阵和网络数据结构。网络和稀疏矩阵数据结构也用于社交图和地图布局。在本章中,我们将涵盖以下主题:

  • 使用图表示的网络

    • 社交网络表示

    • 地图布局

    • 知识图

  • 使用列表的列表表示稀疏矩阵

本章实现了连接人们的社交图,并有一个代码示例展示了如何遍历该图。地图布局通过纬度和经度表示的地理位置进行解释。通过使用汽车及其部件来解释知识图。

技术要求

golang.org/doc/install 为您的操作系统安装 Go 版本 1.10。

本章中代码的 GitHub URL 如下所示:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter09

使用图表示的网络

图是一种通过链接连接对象集的表示。链接连接顶点,即点。图上的基本操作是链接和顶点的添加和删除。以下是一些不同类型的图:

  • 有向图

  • 无向图

  • 连接图

  • 非连接图

  • 简单图

  • 多图

邻接表由具有对象或记录的图的相邻顶点组成。邻接矩阵由源顶点和目标顶点组成。关联矩阵是一个二维布尔矩阵。矩阵有顶点行和表示链接(边)的列。

下面的代码展示了使用图表示的网络。社交图由链接数组组成:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)
// Social Graph
type SocialGraph struct {
  Size int
  Links [][]Link
}

在下一节中定义和实现了 Link 结构体。

链接类

Link 类由 vertex1vertex2 顶点以及 LinkWeight 整数属性组成:

// Link class
type Link struct {
  Vertex1 int
  Vertex2 int
  LinkWeight int
}

下一节将讨论不同 Link 类方法的实现。

NewSocialGraph 方法

NewSocialGraph 函数根据 num 创建社交图,其中 num 是图的尺寸。Size 是图中的链接数量:

// NewSocialGraph method
func NewSocialGraph(num int) *SocialGraph {
  return &SocialGraph{
    Size: num,
    Links: make([][]Link, num),
  }
}

添加链接方法

AddLink 方法在两个顶点之间添加链接。社交图的 AddLink 方法接受 vertex1vertex2weight 作为参数。该方法从 vertex1vertex2 添加链接,如下面的代码所示:

// AddLink method
func (socialGraph *SocialGraph) AddLink(vertex1 int, vertex2 int, weight int) {
  socialGraph.Links[vertex1] = append(socialGraph.Links[vertex1], Link{Vertex1: vertex1, Vertex2: vertex2, LinkWeight: weight})
}

PrintLinks 方法

SocialGraph 类的 PrintLinks 方法打印从 vertex = 0 出发的链接以及图中的所有链接:

// Print Links Example
func (socialGraph *SocialGraph) PrintLinks() {

  var vertex int
  vertex = 0

  fmt.Printf("Printing all links from %d\n", vertex)
  var link Link
  for _, link = range socialGraph.Links[vertex] {
    fmt.Printf("Link: %d -> %d (%d)\n", link.Vertex1, link.Vertex2, link.LinkWeight)
  }

  fmt.Println("Printing all links in graph.")
  var adjacent []Link
  for _, adjacent = range socialGraph.Links {
    for _, link = range adjacent {
      fmt.Printf("Link: %d -> %d (%d)\n", link.Vertex1, link.Vertex2, link.LinkWeight)
    }
  }
}

main 方法

main 方法通过调用 NewSocialGraph 方法创建社交图。将 01021324 的链接添加到社交图中。使用 printLinks 方法打印链接:

// main method
func main() {

  var socialGraph *SocialGraph

  socialGraph = NewSocialGraph(4)

  socialGraph.AddLink(0, 1, 1)
  socialGraph.AddLink(0, 2, 1)
  socialGraph.AddLink(1, 3, 1)
  socialGraph.AddLink(2, 4, 1)

  socialGraph.PrintLinks()

}

执行以下命令以运行 social_graph.go 文件:

go run social_graph.go

输出如下:

图片

在下一节中,我们将查看社交图方法的单元测试。

测试

这里,我们为社交图方法编写了一个单元测试。代码如下:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing testing package
import (
  "fmt"
  "testing"
)
// NewSocialGraph test method
func TestNewSocialGraph(test *testing.T) {

  var socialGraph *SocialGraph

  socialGraph = NewSocialGraph(1)

  if socialGraph == nil {

    test.Errorf("error in creating a social Graph")
  }

}

执行以下命令以运行前面的代码片段:

go test -run NewSocialGraph -v

输出如下:

图片

在下一节中,将使用代码示例实现一个社交网络表示。前述图将增强节点。每个节点将代表一个社交实体。

表示社交网络

社交网络由包含如人、朋友、讨论、分享、信仰、信任和喜好等社交实体的社交链接组成。此图用于表示社交网络。

可以根据图计算与实体邻近度相关的指标。社交图由图节点和链接组成,分别对应具有键名和多个键名的映射:

///Main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

type Name string

type SocialGraph struct {
  GraphNodes map[Name]struct{}
  Links map[Name]map[Name]struct{}
}

下一节将解释并实现不同的社交网络方法。

NewSocialGraph 方法

NewSocialGraph 方法返回一个由 nil 值的 GraphNodesLinks 组成的社交图:

// NewSocialGraph method
func NewSocialGraph() *SocialGraph {
  return &SocialGraph{
    GraphNodes: make(map[Name]struct{}),
    Links: make(map[Name]map[Name]struct{}),
  }
}

AddEntity 方法

AddEntity 方法将实体添加到社交图中。SocialGraph 类的 AddEntity 方法接受 name 作为参数,如果它被添加到社交图中则返回 true

// AddEntity method
func (socialGraph *SocialGraph) AddEntity(name Name) bool {

  var exists bool
  if _, exists = socialGraph.GraphNodes[name]; exists {
    return true
  }
  socialGraph.GraphNodes[name] = struct{}{}
  return true
}

AddLink 方法

SocialGraph 类的 AddLink 方法接受 name1name2 作为参数。如果命名的实体不存在,此方法将创建实体并在实体之间创建链接:

// Add Link
func (socialGraph *SocialGraph) AddLink(name1 Name, name2 Name) {
  var exists bool
  if _, exists = socialGraph.GraphNodes[name1]; !exists {
    socialGraph.AddEntity(name1)
  }
  if _, exists = socialGraph.GraphNodes[name2]; !exists {
    socialGraph.AddEntity(name2)
  }
  if _, exists = socialGraph.Links[name1]; !exists {
    socialGraph.Links[name1] = make(map[Name]struct{})
  }
  socialGraph.Links[name1][name2] = struct{}{}
}

PrintLinks 方法

SocialGraph 类的 PrintLinks 方法打印与 root 相邻的链接以及所有链接,如下面的代码片段所示:

func (socialGraph *SocialGraph) PrintLinks() {
  var root Name
  root = Name("Root") 

  fmt.Printf("Printing all links adjacent to %d\n", root)

  var node Name
  for node = range socialGraph.Links[root] {
    // Edge exists from u to v.
    fmt.Printf("Link: %d -> %d\n", root, node)
  }

  var m map[Name]struct{}
  fmt.Println("Printing all links.")
  for root, m = range socialGraph.Links {
    var vertex Name
    for vertex = range m {
      // Edge exists from u to v.
      fmt.Printf("Link: %d -> %d\n", root, vertex)
    }
  }
}

main 方法

main 方法创建一个社交图。创建实体,如 johnpercynthia,并将它们与根节点链接起来。创建朋友,如 mayolorrieellie,并将它们与 johnper 链接起来:

// main method
func main() {

  var socialGraph *SocialGraph

   socialGraph = NewSocialGraph()

   var root Name = Name("Root")
   var john Name = Name("John Smith")
   var per Name = Name("Per Jambeck")
   var cynthia Name = Name("Cynthia Gibas")

   socialGraph.AddEntity(root)
   socialGraph.AddEntity(john)
   socialGraph.AddEntity(per)
   socialGraph.AddEntity(cynthia)

   socialGraph.AddLink(root, john)
   socialGraph.AddLink(root, per)
   socialGraph.AddLink(root, cynthia)

   var mayo Name = Name("Mayo Smith")
   var lorrie Name = Name("Lorrie Jambeck")
   var ellie Name = Name("Ellie Vlocksen")

   socialGraph.AddLink(john, mayo)
   socialGraph.AddLink(john, lorrie)
   socialGraph.AddLink(per, ellie)

   socialGraph.PrintLinks()
}

执行以下命令以运行 social_graph_example.go 文件:

go run social_graph_example.go

输出如下:

图片

下一节将讨论 地图布局 的实现。

地图布局

地图布局是相互连接的地点的地理可视化。地图的图中的节点由基于地理的信息组成。节点将包含诸如地点名称、纬度和经度等信息。地图以不同的比例展开。地图创建被称为使用地理信息的制图设计。

下面的代码片段显示了地图布局。Place 类包含 NameLatitudeLongitude 属性:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

// Place class
type Place struct {

    Name string
    Latitude float64
    Longitude float64

}

下一个部分将讨论 MapLayout 类。

MapLayout

MapLayout 类由 GraphNodesLinks 组成:

// MapLayout class
type MapLayout struct {
  GraphNodes map[Place]struct{}
  Links map[Place]map[Place]struct{}
}

下一个部分将解释并实现不同的 MapLayout 方法。

The NewMapLayout method

NewMapLayout 方法创建一个 MapLayoutMapLayoutGraphNodes 和链接映射:

// NewMapLayout method
func NewMapLayout() *MapLayout {
  return &MapLayout{
    GraphNodes: make(map[Place]struct{}),
    Links: make(map[Place]map[Place]struct{}),
  }
}

The AddPlace method

MapLayout 类的 AddPlace 方法接受地点作为参数,如果地点存在则返回 true。如果地点不存在,则创建一个新的具有新地点键的图节点:

// AddPlace method
func (mapLayout *MapLayout) AddPlace(place Place) bool {

  var exists bool
  if _, exists = mapLayout.GraphNodes[place]; exists {
    return true
  }
  mapLayout.GraphNodes[place] = struct{}{}
  return true
}

The AddLink method

MapLayout 类的 AddLink 方法接受地点作为参数并将它们连接起来:

// Add Link
func (mapLayout *MapLayout) AddLink(place1 Place, place2 Place) {
  var exists bool
  if _, exists = mapLayout.GraphNodes[place1]; !exists {
    mapLayout.AddPlace(place1)
  }
  if _, exists = mapLayout.GraphNodes[place2]; !exists {
    mapLayout.AddPlace(place2)
  }

  if _, exists = mapLayout.Links[place1]; !exists {
    mapLayout.Links[place1] = make(map[Place]struct{})
  }
  mapLayout.Links[place1][place2] = struct{}{}

}

The PrintLinks method

PrintLinks 方法打印地点和链接:

// PrintLinks method
func (mapLayout *MapLayout) PrintLinks() {
  var root Place
  root = Place{"Algeria", 3, 28}

  fmt.Printf("Printing all links adjacent to %s\n", root.Name)

  var node Place
  for node = range mapLayout.Links[root] {
    fmt.Printf("Link: %s -> %s\n", root.Name, node.Name)
  }

  var m map[Place]struct{}
  fmt.Println("Printing all links.")
  for root, m = range mapLayout.Links {
    var vertex Place
    for vertex = range m {
      fmt.Printf("Link: %s -> %s\n", root.Name, vertex.Name)
    }
  }
}

The main method

main 方法中,通过调用 NewMapLayout 方法创建地图布局。实例化地点并将其添加到地图布局中。然后,在地点之间添加链接:

// main method
func main() {

  var mapLayout *MapLayout

   mapLayout = NewMapLayout()

   var root Place = Place{"Algeria", 3, 28}
   var netherlands Place = Place{"Netherlands", 5.75, 52.5}

   var korea Place = Place{"Korea", 124.1, -8.36}
   var tunisia Place = Place{"Tunisia", 9, 34}

   mapLayout.AddPlace(root)
   mapLayout.AddPlace(netherlands)
   mapLayout.AddPlace(korea)
   mapLayout.AddPlace(tunisia)

   mapLayout.AddLink(root, netherlands)
   mapLayout.AddLink(root,korea)
   mapLayout.AddLink(root,tunisia)

   var singapore Place = Place{"Singapore",103.8,1.36}
   var uae Place = Place{"UAE",54,24}
   var japan Place = Place{"Japan",139.75, 35.68}

   mapLayout.AddLink(korea, singapore)
   mapLayout.AddLink(korea,japan)
   mapLayout.AddLink(netherlands,uae)

   mapLayout.PrintLinks()
}

运行以下命令以执行 map_layout.go 文件:

go run map_layout.go

输出如下:

图片

在下一个部分,我们将查看 NewMapLayout 方法的单元测试。

测试

以下代码片段显示了 MapLayout 类的 NewMapLayout 方法的单元测试:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing testing package
import (
  "testing"
)

// NewMapLayout test method
func TestNewMapLayout(test *testing.T) {

  var mapLayout *MapLayout

  mapLayout = NewMapLayout()

  test.Log(mapLayout)

  if mapLayout == nil {

    test.Errorf("error in creating a mapLayout")
  }

}

运行以下命令以执行前面的代码片段:

go test -run NewMapLayout -v

输出如下:

图片

下一个部分将讨论实现知识图谱

知识图谱

知识图谱是实体、物品和用户作为节点的网络表示。节点通过链接或边相互交互。知识图谱因其无模式性而被广泛使用。这些数据结构用于以图形形式表示知识,节点包含文本信息。知识图谱通过使用项目、实体和用户节点并通过边将它们连接起来创建。

本体由信息节点构成的知识图谱。推理器从知识图谱中推导出知识。知识图谱由类、槽位和方面组成,这些是本体术语。在以下代码中,一个由汽车物料清单构成的知识图谱被解释。Class 类型包含一个名称,它是一个字符串:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

// Class Type
type Class struct {
  Name string
}

下一个部分将讨论 knowledge graph 类。

KnowledgeGraph

KnowledgeGraph 类由 GraphNodes 和链接组成:

// Knowledge Graph type
type KnowledgeGraph struct {
  GraphNodes map[Class]struct{}
  Links map[Class]map[Class]struct{}
}

以下各节解释并实现了不同的知识图谱方法。

NewKnowledgeGraph 方法

NewKnowledgeGraph 方法创建一个知识图谱,它由 GraphNodesLinks 映射组成。

// NewKnowledgeGraph method
func NewKnowledgeGraph() *KnowledgeGraph {
  return &KnowledgeGraph{
    GraphNodes: make(map[Class]struct{}),
    Links: make(map[Class]map[Class]struct{}),
  }
}

AddClass 方法

KnowledgeGraph 类的 AddClass 方法接受 class 作为参数,如果类存在则返回 true。如果类不存在,则创建一个以 class 为键的 GraphNode

// AddClass method
func (knowledgeGraph *KnowledgeGraph) AddClass(class Class) bool {

  var exists bool
  if _, exists = knowledgeGraph.GraphNodes[class]; exists {
    return true
  }
  knowledgeGraph.GraphNodes[class] = struct{}{}
  return true
}

AddLink 方法

KnowledgeGraph 类的 AddLink 方法接受 class1class2 作为参数,并在这些类之间创建链接:

// Add Link
func (knowledgeGraph *KnowledgeGraph) AddLink(class1 Class, class2 Class) {
  var exists bool
  if _, exists = knowledgeGraph.GraphNodes[class1]; !exists {
    knowledgeGraph.AddClass(class1)
  }
  if _, exists = knowledgeGraph.GraphNodes[class2]; !exists {
    knowledgeGraph.AddClass(class2)
  }

  if _, exists = knowledgeGraph.Links[class1]; !exists {
    knowledgeGraph.Links[class1] = make(map[Class]struct{})
  }
  knowledgeGraph.Links[class1][class2] = struct{}{}

}

PrintLinks 方法

KnowledgeGraph 类的 PrintLinks 方法打印链接和节点:

// Print Links method
func (knowledgeGraph *KnowledgeGraph) PrintLinks() {
  var car Class
  car = Class{"Car"}

  fmt.Printf("Printing all links adjacent to %s\n", car.Name)

  var node Class
  for node = range knowledgeGraph.Links[car] {
    fmt.Printf("Link: %s -> %s\n", car.Name, node.Name)
  }

  var m map[Class]struct{}
  fmt.Println("Printing all links.")
  for car, m = range knowledgeGraph.Links {
    var vertex Class
    for vertex = range m {
      fmt.Printf("Link: %s -> %s\n", car.Name, vertex.Name)
    }
  }
}

main 方法

main 方法创建知识图谱,并实例化类。创建并打印类之间的链接:

// main method
func main() {

  var knowledgeGraph *KnowledgeGraph

  knowledgeGraph = NewKnowledgeGraph()

  var car = Class{"Car"}
  var tyre = Class{"Tyre"}
  var door = Class{"Door"}
  var hood = Class{"Hood"}

  knowledgeGraph.AddClass(car)
  knowledgeGraph.AddClass(tyre)
  knowledgeGraph.AddClass(door)
  knowledgeGraph.AddClass(hood)

  knowledgeGraph.AddLink(car, tyre)
  knowledgeGraph.AddLink(car, door)
  knowledgeGraph.AddLink(car, hood)

  var tube = Class{"Tube"}
  var axle = Class{"Axle"}
  var handle = Class{"Handle"}
  var windowGlass = Class{"Window Glass"}

  knowledgeGraph.AddClass(tube)
  knowledgeGraph.AddClass(axle)
  knowledgeGraph.AddClass(handle)
  knowledgeGraph.AddClass(windowGlass)

  knowledgeGraph.AddLink(tyre, tube)
  knowledgeGraph.AddLink(tyre, axle)
  knowledgeGraph.AddLink(door, handle)
  knowledgeGraph.AddLink(door, windowGlass)

  knowledgeGraph.PrintLinks()
}

执行以下命令以运行 knowledge_catalog.go 文件:

go run knowledge_catalog.go

输出如下:

图片

在下一节中,我们将查看 NewKnowledgeGraph 方法的单元测试。

测试

以下代码片段对 NewKnowledgeGraph 方法进行了单元测试:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing testing package
import (
  "testing"
)

// NewKnowledgeGraph test method
func TestNewKnowledgeGraph(test *testing.T) {

  var knowledgeGraph *KnowledgeGraph

  knowledgeGraph = NewKnowledgeGraph()

  test.Log(knowledgeGraph)

  if knowledgeGraph == nil {

    test.Errorf("error in creating a knowledgeGraph")
  }

}

执行以下命令以运行前面的代码片段:

go test -run NewKnowledgeGraph -v

输出如下:

图片

下一节讨论稀疏矩阵的表示。

使用列表的列表表示稀疏矩阵

稀疏矩阵是一个由 m 行和 n 列组成的二维列表。如果矩阵由 m 行和 n 列组成,则其形状为 m x n。稀疏矩阵用于解决不需要密集矩阵的大规模问题。例如,通过使用有限元方法FEM)求解偏微分方程。稀疏矩阵的元组是矩阵的非零元素。

在以下代码中,稀疏矩阵被建模为一个列表的列表。稀疏矩阵由一系列列表的列表组成。每个单元格具有 RowColumnValue 等属性:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
  "fmt"
)

//List of List
type LOL struct {
  Row int
  Column int
  Value float64
}

下一节将讨论 SparseMatrix 类。

SparseMatrix

SparseMatrix 有一个 cells 数组和 shape,它是一个整数数组:

//Sparse Matrix
type SparseMatrix struct {
  cells []LOL
  shape [2]int
}

在下一节中,将实现 SparseMatrix 结构的不同 Sparse 方法。

Shape 方法

SparseMatrix 类的 Shape 方法返回 shape 数组元素:

// Shape method
func (sparseMatrix *SparseMatrix) Shape() (int, int) {
  return sparseMatrix.shape[0], sparseMatrix.shape[1]
}

NumNonZero 方法

NumNonZero 方法找到具有非零元素的单元格。SparseMatrix 类的 NumNonZero 方法返回 sparseMatrixcells 数组的大小:

// NumNonZero method
func (sparseMatrix *SparseMatrix) NumNonZero() int {
  return len(sparseMatrix.cells)
}

LessThan 方法

LessThan 方法比较列表的行和列,并检查行是否小于 i 以及列是否小于 j

// Less Than method
func LessThan(lol LOL, i int, j int) bool {

  if lol.Row < i && lol.Column < j {

    return true
  }

  return false
}

Equal 方法

Equal 方法检查列表的列表行和列是否分别等于 ij

// Equal method
func Equal(lol LOL, i int, j int) bool {
  if lol.Row == i && lol.Column == j {
    return true
  }
  return false
}

GetValue 方法

SparseMatrix类的GetValue方法返回行和列分别等于ij的单元格的值:

// GetValue method
func (sparseMatrix *SparseMatrix) GetValue(i int, j int) float64 {
  var lol LOL
  for _, lol = range sparseMatrix.cells {
    if LessThan(lol, i, j) {
      continue
    }
    if Equal(lol, i, j) {
      return lol.Value
    }
    return 0.0
  }
  return 0.0
}

SetValue方法

SparseMatrix类的SetValue方法将行和列等于ij的单元格的值设置为参数值:

//SetValue method
func (sparseMatrix *SparseMatrix) SetValue(i int, j int, value float64) {

  var lol LOL
  var index int
  for index, lol = range sparseMatrix.cells {
    if LessThan(lol, i, j) {
      continue
    }
    if Equal(lol, i, j) {
      sparseMatrix.cells[index].Value = value
      return
    }

    sparseMatrix.cells = append(sparseMatrix.cells, LOL{})
    var k int
    for k = len(sparseMatrix.cells) - 2; k >= index; k-- {
      sparseMatrix.cells[k+1] = sparseMatrix.cells[k]
    }
    sparseMatrix.cells[index] = LOL{
      Row: i,
      Column: j,
      Value: value,
    }
    return
  }
  sparseMatrix.cells = append(sparseMatrix.cells, LOL{
    Row: i,
    Column: j,
    Value: value,
  })
}

新的稀疏矩阵方法

NewSparseMatrix方法接受mn作为参数,并返回初始化后的矩阵:

// New SparseMatrix method
func NewSparseMatrix(m int, n int) *SparseMatrix {
  return &SparseMatrix{
    cells: []LOL{},
    shape: [2]int{m, n},
  }
}

主要方法

main方法通过调用NewSparseMatrix方法创建稀疏矩阵。值设置在单元格(1, 1)和(1, 3)中。打印出稀疏矩阵和非零单元格的数量:

// main method
func main() {

  var sparseMatrix *SparseMatrix

  sparseMatrix = NewSparseMatrix(3, 3)

  sparseMatrix.SetValue(1, 1, 2.0)
  sparseMatrix.SetValue(1, 3, 3.0)

  fmt.Println(sparseMatrix)
  fmt.Println(sparseMatrix.NumNonZero())
}

运行以下命令以执行sparse_matrix.go文件:

go run sparse_matrix.go

输出如下:

图片

摘要

本章介绍了如何分别使用图和列表的列表来表示网络和稀疏矩阵。详细讨论了社交网络表示、地图布局和知识图,并提供了代码示例。还实现了不同的稀疏矩阵方法,并附有相应的代码。

在下一章中,将通过代码示例和效率分析介绍垃圾回收、缓存管理和空间分配等算法。

问题

  • 用于表示一组链接对象的什么数据结构?

  • 带有布尔值的二维矩阵叫什么?

  • 使用图表示网络的一个代码示例。

  • 可以从社交图中计算出哪些度量?

  • 什么是制图设计?

  • 提供一个知识图的示例并定义类、槽位和面。

  • 稀疏矩阵有哪些应用?

  • 定义一个列表的列表并编写一个代码示例。

  • 什么是地图布局?

  • 可以使用图执行哪些不同的操作?

进一步阅读

如果你想了解更多关于图和列表的书籍,以下几本书推荐:

  • 《设计模式》,作者:艾里克·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利斯

  • 《算法导论 第 3 版》,作者:托马斯·H·科莫恩、查尔斯·E·莱伊森森、罗纳德·L·里维斯和克利福德·斯坦

  • 《数据结构和算法:简单入门》,作者:鲁道夫·拉塞尔

第十章:内存管理

内存管理是一种控制和组织内存的方式。内存分区称为,它们用于运行不同的进程。内存管理算法的基本目标是根据需求动态地为程序分配内存段。当内存中的对象不再需要时,算法会释放内存。垃圾收集、缓存管理和空间分配算法是内存管理技术的良好示例。在软件工程中,垃圾收集用于释放分配给那些不再使用的对象的内存,从而帮助进行内存管理。缓存为数据提供内存存储。您可以将缓存中的数据按地域分组排序。数据可以使用键值集进行存储。

本章涵盖了垃圾收集、缓存管理和空间分配算法。内存管理算法通过代码示例和效率分析进行展示。本章将涵盖以下主题:

  • 垃圾收集

  • 缓存管理

  • 空间分配

  • 概念——Go 内存管理

我们将首先查看垃圾收集,然后查看与垃圾收集相关的不同算法。

技术要求

从 Golang(golang.org/)安装 Go 版本 1.10,选择适合您操作系统的正确版本。

本章代码的 GitHub 仓库可以在这里找到:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Chapter10

垃圾收集

垃圾收集是一种程序化内存管理,其中收集当前由永远不会再次使用的对象占用的内存。约翰·麦卡锡是第一个提出垃圾收集来管理 Lisp 内存管理的人。该技术指定了哪些对象需要被释放,然后释放内存。用于垃圾收集的策略包括栈分配区域干扰。套接字、关系数据库句柄、用户窗口对象和文件资源不受垃圾收集器的监管。

垃圾收集算法有助于减少悬挂指针缺陷、双重释放缺陷和内存泄漏。这些算法计算密集,会导致性能下降或不均匀。据苹果公司称,iOS 没有垃圾收集的原因之一是垃圾收集需要五倍的内存来匹配显式内存管理。在高交易系统中,并发、增量实时垃圾收集器有助于管理内存收集和释放。

垃圾收集算法依赖于各种因素:

  • GC 吞吐量

  • 堆开销

  • 暂停时间

  • 暂停频率

  • 暂停分布

  • 分配性能

  • 压缩

  • 并发

  • 规模化

  • 调优

  • 预热时间

  • 页面释放

  • 可移植性

  • 兼容性

那就是简单、延迟、单比特、加权引用计数、标记-清除和代际收集算法,这些将在以下几节中讨论。

ReferenceCounter

以下代码片段显示了创建的对象引用如何在栈中维护。ReferenceCounter类具有引用数量属性,包括引用池和已删除引用:

//main package has examples shown
// in Hands-On Data Structures and algorithms with Go book
package main

// importing fmt package
import (
    "fmt"
    "sync"
)

//Reference Counter
type ReferenceCounter struct {
    num *uint32
    pool *sync.Pool
    removed *uint32
}

让我们看看ReferenceCounter类的方法。

newReferenceCounter方法

newReferenceCounter方法初始化一个ReferenceCounter实例,并返回对ReferenceCounter的指针。这在上面的代码中显示:

//new Reference Counter method
func newReferenceCounter() *ReferenceCounter {
    return &ReferenceCounter{
    num: new(uint32),
    pool: &sync.Pool{},
    removed: new(uint32),
    }
}

在下一节中描述了Stack类。

Stack

Stack类由references数组和Count属性组成。这在上面的代码中显示:

// Stack class
type Stack struct {
    references []*ReferenceCounter
    Count int
}

让我们看看Stack类的实现方法。

Stack类 – 新方法

现在,让我们看看Stack类实现的堆接口方法。新方法初始化references数组,PushPop堆接口方法使用reference参数将reference从栈中推入和弹出。这在上面的代码中显示:

// New method of Stack Class
func (stack *Stack) New() {
    stack.references = make([]*ReferenceCounter,0)
}

// Push method
func (stack *Stack) Push(reference *ReferenceCounter) {
    stack.references = append(stack.references[:stack.Count], 
    reference)
    stack.Count = stack.Count + 1
}

// Pop method
func (stack *Stack) Pop() *ReferenceCounter {
    if stack.Count == 0 {
        return nil
    }
var length int = len(stack.references)
var reference *ReferenceCounter = stack.references[length -1]
if length > 1 {
  stack.references = stack.references[:length-1]
  } else {
  stack.references = stack.references[0:]
}
    stack.Count = len(stack.references)
    return reference
}

主要方法

在以下代码片段中,让我们看看如何使用Stack。初始化一个Stack实例,通过调用Push方法将引用添加到栈中。调用Pop方法并打印输出:

// main method
func main() {
    var stack *Stack = &Stack{}
    stack.New()
    var reference1 *ReferenceCounter = newReferenceCounter()
    var reference2 *ReferenceCounter = newReferenceCounter()
    var reference3 *ReferenceCounter = newReferenceCounter()
    var reference4 *ReferenceCounter = newReferenceCounter()
    stack.Push(reference1)
    stack.Push(reference2)
    stack.Push(reference3)
    stack.Push(reference4)
    fmt.Println(stack.Pop(), stack.Pop(), stack.Pop(), stack.Pop())
}

执行以下命令以运行stack_garbage_collection.go文件:

go run stack_garbage_collection.go

输出如下:

图片

在以下几节中,将讨论引用计数、标记-清除和代际收集算法。

引用计数

引用计数是一种用于跟踪引用、指针和资源句柄数量的技术。内存块、磁盘空间和对象是资源的良好例子。该技术将每个对象视为资源。跟踪的指标是不同对象持有的引用数量。当对象无法再次被引用时,对象将被恢复。

引用数量用于运行时优化。Deutsch-Bobrow 提出了引用计数的策略。这个策略与放入局部变量的引用产生的更新引用数量有关。Henry Baker 提出了一种方法,该方法包括延迟到需要时才包含在局部变量中的引用。

在以下小节中,将讨论引用计数的简单、延迟、单比特和加权技术。

简单引用计数

引用计数与跟踪资源(如对象、内存块或磁盘空间)的引用数量有关。这项技术与不再被引用的已分配对象的引用数量有关。

收集技术跟踪每个对象对对象的引用数量。引用由其他对象持有。当对象的引用数量为零时,对象被移除。被移除的对象变得不可访问。引用的移除可以触发无数相关引用的清除。

由于对象图的大小和缓慢的访问速度,该算法耗时。

在以下代码片段中,我们可以看到一个简单的引用计数算法的实现。ReferenceCounter 类具有数字(num)、poolremoved 引用作为属性:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing sync, atomic and fmt packages
import (
    "sync/atomic"
    "sync"
    "fmt"
)

//Reference Counter
type ReferenceCounter struct {
    num *uint32
    pool *sync.Pool
    removed *uint32
}

以下代码片段显示了 ReferenceCounter 类的 newReferenceCounterAddSubtract 方法:

//new Reference Counter method
func newReferenceCounter() ReferenceCounter {
    return ReferenceCounter{
        num: new(uint32),
        pool: &sync.Pool{},
        removed: new(uint32),
    }
}

// Add method
func (referenceCounter ReferenceCounter) Add() {
    atomic.AddUint32(referenceCounter.num, 1)
}

// Subtract method
func (referenceCounter ReferenceCounter) Subtract() {
    if atomic.AddUint32(referenceCounter.num, ^uint32(0)) == 0 {
        atomic.AddUint32(referenceCounter.removed, 1)
    }
}

让我们看看 main 方法,并查看一个简单的引用计数的示例。调用 newReferenceCounter 方法,并通过调用 Add 方法添加引用。最后打印 count 引用。这在上面的代码片段中显示。

// main method
func main() {
    var referenceCounter ReferenceCounter
    referenceCounter = newReferenceCounter()
    referenceCounter.Add()
    fmt.Println(*referenceCounter.count)
}

执行以下命令以运行 reference_counting.go 文件:

go run reference_counting.go

输出如下:

图片

以下章节描述了不同类型的引用计数技术。

延迟引用计数

延迟引用计数是一种检查来自不同对象到指定对象的引用并忽略程序变量引用的程序。如果引用计数为零,则不会考虑该对象。此算法有助于减少保持计数更新的开销。延迟引用计数被许多编译器支持。

一位引用计数

一位引用计数技术使用一个单独的位标志来表示对象是否有一个或多个引用。该标志作为对象指针的一部分存储。在此技术中,无需为额外空间预留任何对象。由于大多数对象的引用计数为 1,因此该技术是可行的。

加权引用计数

加权引用计数技术统计对象引用的数量,并为每个引用分配一个权重。该技术跟踪对象引用的总权重。加权引用计数技术由 Bevan、Watson 和 Watson 在 1987 年发明。以下代码片段展示了加权引用计数技术的实现:

//Reference Counter
type ReferenceCounter struct {
    num *uint32
    pool *sync.Pool
    removed *uint32
    weight int
}

//WeightedReference method
func WeightedReference() int {
    var references []ReferenceCounter
    references = GetReferences(root)
    var reference ReferenceCounter
    var sum int
    for _, reference = range references {
        sum = sum + reference.weight
    }
    return sum
}

标记-清除算法

标记-清除算法基于 1978 年由迪杰斯特拉提出的想法。在垃圾回收风格中,堆由一个由白色对象组成的连接图组成。这种技术遍历对象并检查它们是否被应用程序特别使用。在这个技术中,全局变量和栈上的对象被着色为灰色。每个灰色对象都会被加深为黑色并过滤出指向其他对象的指针。在输出中找到的任何白色对象都会变为灰色。这个计算会重新进行,直到没有灰色对象为止。未被包含的白色对象是不可访问的。

在此算法中,一个突变者通过在收集器运行时更改指针来处理并发。它还负责确保没有黑色对象指向白色对象。标记算法有以下步骤:

  1. 标记root对象

  2. 如果位的值为false,则将root位标记为true

  3. 对于每个root引用,标记引用,就像第一步一样

以下代码片段显示了标记算法。让我们看看Mark方法的实现:

func Mark( root *object){
   var markedAlready bool
   markedAlready = IfMarked(root)
   if !markedAlready {
        map[root] = true
   }
   var references *object[]
   references = GetReferences(root)
   var reference *object
   for _, reference = range references {
       Mark(reference)
   }
}

扫描算法的伪代码如下所示:

  • 对于堆中的每个对象,如果位的值为true,则将该位标记为false

  • 如果位的值为true,则从堆中释放对象

扫描算法释放了标记为垃圾回收的对象。

现在,让我们看看扫描算法的实现:

func Sweep(){
   var objects *[]object
   objects = GetObjects()
   var object *object
   for _, object = range objects {
   var markedAlready bool
   markedAlready = IfMarked(object)
   if markedAlready {
        map[object] = true
   }
       Release(object)
   }
}

世代收集算法

世代收集算法将对象堆分为世代。根据对象的年龄,算法将使一代对象过期并被收集。根据垃圾收集周期中对象的年龄,算法将对象提升到较老的世代。

即使收集了一代,也需要扫描整个堆。假设收集了3代;在这种情况下,0-2代也会被扫描。世代收集算法在以下代码片段中展示:

func GenerationCollect(){
   var currentGeneration int
   currentGeneration = 3
   var objects *[]object
   objects = GetObjectsFromOldGeneration(3)
   var object *object
   for _, object = range objects {
       var markedAlready bool
       markedAlready = IfMarked(object)
       if markedAlready {
           map[object] = true
       }
    }
}

我们将在下一节中查看缓存管理。

缓存管理

缓存管理包括管理静态、动态和变量信息:

  • 静态信息永远不会改变

  • 动态信息变化频繁

  • 变量信息比动态信息变化频率低

对象缓存存储在各种数据结构中,如映射和树。映射有一个作为标识符的键和一个作为对象的值。

缓存对象可以与内存、磁盘、池和流相关联。缓存具有与生存时间、组和区域相关的属性。区域由一组映射的键值对组成。区域可以独立于其他区域。缓存配置包括默认值、区域和辅助工具。

典型的缓存管理器具有以下功能:

  • 内存管理

  • 线程池控制

  • 元素分组

  • 可配置的运行时参数

  • 区域数据分离和配置

  • 远程同步

  • 远程存储恢复

  • 事件处理

  • 远程服务器链路和故障转移

  • 自定义事件记录

  • 自定义事件队列注入

  • 关键模式匹配检索

  • 网络高效的多键检索

在以下部分将描述 CacheObject 类和 Cache 类。

CacheObject 类

CacheObject 类具有 ValueTimeToLive 属性。这将在以下代码中展示:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt, sync and time packages
import (
    "fmt"
    "sync"
    "time"
)

// CacheObject class
type CacheObject struct {
    Value string
    TimeToLive int64
}

CacheObject 类的 IfExpired 方法将在下一节展示。

IfExpired 方法

IfExpired 检查 cache 对象是否已过期。如果 TimeToLive 未过期,CacheObjectIfExpired 方法返回 true;否则,返回 false。这将在以下代码中展示:

// IfExpired method
func (cacheObject CacheObject) IfExpired() bool {
    if cacheObject.TimeToLive == 0 {
        return false
    }
    return time.Now().UnixNano() > cacheObject.TimeToLive
}

缓存类

Cache 类由具有 string 键、CacheObject 值和 sync.RWMutex 锁的 objects map 组成。这将在以下代码中展示:

//Cache class
type Cache struct {
    objects map[string]CacheObject
    mutex *sync.RWMutex
}

Cache 类的 NewCacheGetObjectSetValue 方法将在以下部分展示。

NewCache 方法

NewCache 方法返回一个指向缓存的指针,该缓存使用 nil 映射(即没有值的映射)和 RWMutex 初始化。这将在以下代码中展示:

//NewCache method
func NewCache() *Cache {
    return &Cache{
        objects: make(map[string]CacheObject),
        mutex: &sync.RWMutex{},
    }
}

GetObject 方法

GetObject 方法根据缓存键检索对象。Cache 类的 GetObject 方法返回 cacheKey 的值。在返回 cacheKey 的值之前,在缓存的 mutex 对象上调用 RLock 方法,并延迟调用 RUnlock 方法。如果对象已过期,键值将是一个空字符串。这将在以下代码中展示:

//GetObject method
func (cache Cache) GetObject(cacheKey string) string {
    cache.mutex.RLock()
    defer cache.mutex.RUnlock()
    var object CacheObject
    object = cache.objects[cacheKey]
    if object.IfExpired() {
        delete(cache.objects, cacheKey)
    return ""
    }
    return object.Value
}

SetValue 方法

Cache 类的 SetValue 方法接受 cacheKeycacheValuetimeToLive 参数。在缓存的 mutex 对象上调用 Lock 方法,并延迟调用 Unlock 方法。使用 cacheValueTimeToLive 作为属性创建一个新的 CacheObject。创建的 cacheObject 被设置为具有 cacheKey 键的映射对象的值。这将在以下代码中展示:

//SetValue method
func (cache Cache) SetValue(cacheKey string, cacheValue string, timeToLive time.Duration) {
    cache.mutex.Lock()
    defer cache.mutex.Unlock()
    cache.objects[cacheKey] = CacheObject{
        Value: cacheValue,
        TimeToLive: time.Now().Add(timeToLive).UnixNano(),
    }
}

我们将在下一节中在 main 方法中实现我们刚刚查看的方法。

main 方法

main 方法通过调用 NewCache 方法创建缓存。通过调用 setValue 在缓存上设置键和值。通过调用 Cache 类的 GetObject 方法访问值。这将在以下代码中展示:

// main method
func main() {
    var cache *Cache
    cache = NewCache()
    cache.SetValue("name", "john smith", 200000000)
    var name string
    name = cache.GetObject("name")
    fmt.Println(name)
}

运行以下命令以执行 cache_management.go 文件:

go run cache_management.go

输出如下:

图片

下一节将讨论空间分配算法。

空间分配

每个函数都与单个内存空间关联的堆栈帧。函数可以访问帧内的内存,并且一个帧指针指向内存的位置。当函数被调用时发生帧之间的转换。在转换期间,数据通过值从一帧传递到另一帧。

以下代码展示了栈帧创建和内存分配。addOne 函数接收 num 并将其增加 1。函数打印 num 的值和地址:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)

// increment method
func addOne(num int) {
    num++
    fmt.Println("added to num", num, "Address of num", &num)
}

main 方法将变量数量初始化为 17。在调用 addOne 函数前后,打印了数值和地址。这在上面的代码中显示:

// main method
func main() {
    var number int
    number = 17
    fmt.Println("value of number", number, "Address of number", 
    &number)
    addOne(number)
    fmt.Println("value of number after adding One", number, "Address 
    of", &number)
}

执行以下命令以运行 stack_memory_allocation.go 文件:

go run stack_memory_allocation.go

输出如下:

下一节将解释帧指针。

指针

指针的地址长度为 4 或 8 字节,具体取决于你是否有一个 32 位或 64 位架构。栈的主要帧由数字 17 和地址 0xc420016058 组成。在加一之后,创建了一个新的帧,其中 num 等于 18,地址为 0xc420016068main 方法在调用 addOne 函数后打印栈的主要帧。以下代码部分展示了使用指针而不是实际传递给函数的值进行内存空间分配。

以下部分展示了指针的 AddOnemain 方法。

addOne 方法

addOne 函数接收 num 的指针并增加 1。函数打印 num 的值、地址和指针。这在上面的代码中显示:

///main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt package
import (
    "fmt"
)

// increment method
func addOne(num *int) {
    *num++
    fmt.Println("added to num", num, "Address of num", &num, "Value 
    Points To", *num )
}

main 方法

main 方法将变量 number 初始化为 17。将数字的指针传递给 addOne 函数。在调用 addOne 函数前后,打印了数值和地址。

在此示例中,数字的地址与 addOne 函数中 num 的值相同。指针共享变量的地址,以便函数在栈帧内进行读写操作。指针类型对每个声明的类型都是特定的。指针提供了在函数栈帧之外进行间接内存访问的能力。这在上面的代码中显示:

// main method
func main() {
    var number int
    number = 17
    fmt.Println("value of number", number, "Address of number", 
    &number)
    addOne(&number)
    fmt.Println("value of number after adding One", number, "Address 
    of", &number)
}

执行以下命令以运行 stack_memory_pointer.go 文件:

go run stack_memory_pointer.go

输出如下:

下一节将讨论 Go 中的内存管理。

概念 – Go 内存管理

在 Go 中,程序员不需要担心在内存和空间分配中编码变量的值放置。Go 中的垃圾回收由内存管理器负责。GOGC 变量用于设置初始垃圾回收目标百分比的值。当新鲜分配的数据与之前垃圾回收后剩余的存活数据的比例达到目标百分比时,垃圾回收被激活。GOGC 变量的默认值为 100。此设置可以关闭,这将停止垃圾回收。Go 中垃圾回收的当前实现使用 标记-清除 算法。

你可以遵循的一些最佳实践来提高内存管理如下:

  • 小对象可以组合成更大的对象

  • 超出其声明作用域的局部变量可以被提升为堆分配

  • 可以执行切片数组预分配来提高内存

  • 使用int8而不是int,因为int8是一种更小的数据类型

  • 没有任何指针的对象不会被垃圾回收器扫描

  • 可以使用 FreeLists 来重用瞬态对象并减少分配的数量

性能分析

在 Go 中,可以通过使用cpuprofilememprofile标志来启用性能分析。Go 测试包支持基准测试和性能分析。可以通过以下命令调用cpuprofile标志:

go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprofile net/http

可以使用以下命令将基准测试写入cprofile输出文件:

go tool pprof --text http.test cprof

让我们看看如何对所编写的程序进行性能分析。flag.Parse方法读取命令行标志。CPU 性能分析输出被写入文件。在程序停止之前,需要写入任何挂起的文件输出,此时将调用分析器的StopCPUProfile方法:

var profile = flag.String("cpuprofile", "", "cpu profile to output file")
func main() {
    flag.Parse()
    if *profile != "" {
        var file *os.File
        var err error
        file, err = os.Create(*profile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(file)
        defer pprof.StopCPUProfile()
    }

摘要

本章涵盖了垃圾回收、缓存管理和内存空间分配算法。我们探讨了包括简单、延迟、单比特和加权在内的引用计数算法。还展示了标记-清除和代际收集算法的代码示例。

下一章将介绍在阅读完本书之后我们可以采取的下一步行动。

问题

  1. 选择垃圾回收算法时考虑哪些因素?

  2. 在哪种引用计数算法中,程序变量引用被忽略?

  3. 在哪种引用计数算法中使用单比特标志进行计数?

  4. 在哪种引用计数算法中,每个引用都被分配了一个权重?

  5. 谁发明了加权引用计数?

  6. 迪杰斯特拉提出了哪种垃圾回收算法?

  7. 当标记-清除收集器运行时,哪个类处理并发?

  8. 提升对象到较老代的标准是什么?

  9. 绘制缓存管理算法的流程图。

  10. 如何在方法栈帧外获取间接内存访问?

进一步阅读

如果你想了解更多关于垃圾回收的信息,以下书籍推荐:

  • 《设计模式》,作者:艾里克·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利斯

  • 《算法导论 第 3 版》,作者:托马斯·H·科门、查尔斯·E·莱伊森、罗纳德·L·里维斯和克利福德·斯坦

  • 《数据结构和算法:简单入门》,作者:鲁道夫·拉斯尔

第十一章:下一步

在本附录中,我们分享了读者的学习成果。展示了代码仓库链接和关键要点。包括最新数据结构和算法的参考文献。提供提示和技术,帮助你跟上数据结构和算法的最新动态。

技术要求

golang.org/doc/install安装 Go 版本 1.10,确保选择适合您操作系统的正确版本。

本附录中的代码 GitHub 仓库可在此找到:github.com/PacktPublishing/Learn-Data-Structures-and-Algorithms-with-Golang/tree/master/Appendix

成果

本书的成果如下:

  • 使用正确的数据结构和算法提高网络或移动应用程序的性能。

  • 理解算法如何解决问题,以及如何为问题选择正确的数据结构。

  • 列举问题的各种解决方案,并在进行成本/效益分析后识别算法和数据结构。

  • 掌握编写算法伪代码的各种技巧,让你在白板会议和面试任务中脱颖而出。

  • 通过预测算法或数据结构的速度和效率来预测选择数据结构和算法的陷阱。

在下一节中,讨论了关键要点、参考文献、文章以及提示和技术。

关键要点

读者的关键要点如下:

  • 如何为问题选择正确的算法和数据结构。

  • 如何比较不同算法的复杂性和数据结构,以评估代码性能和效率。

  • 如何应用最佳实践来提高和增强应用程序的性能。

  • 书中提供了与网络和移动软件解决方案相关的现实世界问题、解决方案和最佳实践,作为代码示例。

下一步

在本节中,为每一章提供了进一步阅读的论文和文章。

第一章 – 数据结构与算法

以下文章与数据结构和算法相关:

以下论文与数据结构和算法相关:

第二章 – Go 数据结构与算法入门

以下文章与本章内容相关:

第三章 – 线性数据结构

以下文章与线性数据结构相关:

以下论文与线性数据结构相关:

第四章 – 非线性数据结构

以下文章与非线性数据结构相关:

以下论文与非线性数据结构相关:

第五章 – 同质数据结构

以下文章与同质数据结构相关:

第六章 – 异质数据结构

以下文章与异质数据结构相关:

第七章 – 动态数据结构

以下文章与动态数据结构相关:

以下论文与动态数据结构相关:

第八章 – 经典算法

以下文章与经典算法相关:

以下论文与经典算法相关:

第九章 – 网络和稀疏矩阵表示

以下文章与网络和稀疏矩阵表示相关:

以下论文与网络和稀疏矩阵表示相关:

在第九章,网络和稀疏矩阵表示中,展示了来自实际应用的用例。了解网络数据结构和稀疏矩阵在不同领域(如航空公司、银行、医疗、制药、电信和供应链)中的应用,对于读者来说是一个很好的下一步。

第十章 – 内存管理

以下文章与内存管理相关:

以下论文与内存管理相关:

下一个章节将讨论在 Go 数据结构和算法中使用的不同技巧和技术。

技巧和技术

要保持对 Go 的关注,可以订阅这些论坛和博客:

以下部分包含编写 Go 代码的技巧。

使用具有超时间隔的通道

连接到资源的软件程序可以设置超时。通道用于实现超时。你可以按照以下方式配置具有超时间隔的通道:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing errors, log and time packages
import (
    "errors"
    "log"
    "time"
)

// delayTimeOut method
func delayTimeOut(channel chan interface{}, timeOut time.Duration) (interface{}, error) {
    log.Printf("delayTimeOut enter")
    defer log.Printf("delayTimeOut exit")
    var data interface{}
    select {
        case <-time.After(timeOut):
        return nil, errors.New("delayTimeOut time out")
        case data = <-channel:
        return data, nil
    }
}

//main method
func main() {
    channel := make(chan interface{})
    go func() {
        var err error
        var data interface{}
        data, err = delayTimeOut(channel, time.Second)
        if err != nil {
            log.Printf("error %v", err)
            return
        }
        log.Printf("data %v", data)
    }()
    channel <- struct{}{}
    time.Sleep(time.Second * 2)
    go func() {
        var err error
        var data interface{}
        data, err = delayTimeOut(channel, time.Second)
        if err != nil {
            log.Printf("error %v", err)
            return
        }
        log.Printf("data %v", data)
    }()
    time.Sleep(time.Second * 2)
}

执行以下命令以运行前面的代码片段:

go run chan_timeout.go

输出如下:

使用上下文而不是通道

在 Go 线程中执行的功能可以实现上下文。上下文用于在代码中传递进程间信息,而不是使用通道。以下代码片段展示了上下文的用法:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing errors,context,log and time packages

import (
  "errors"
  "golang.org/x/net/context"
  "log"
  "time"
)

// main method
func main() {

  var delay time.Duration

  delay = time.Millisecond

  var cancel context.CancelFunc

  var contex context.Context

  contex, cancel = context.WithTimeout(context.Background(), delay)

  go func(context.Context) {
    <-contex.Done()
    log.Printf("contex done")
  }(contex)

  _ = cancel

  time.Sleep(delay * 2)

  log.Printf("contex end %v", contex.Err())

  channel := make(chan struct{})

  var err error
  go func(chan struct{}) {
    select {
    case <-time.After(delay):
      err = errors.New("ch delay")
    case <-channel:
    }
    log.Printf("channel done")
  }(channel)

  time.Sleep(delay * 2)

  log.Printf("channel end %v", err)
}

执行以下命令以运行前面的代码片段:

go run context.go

输出如下:

Panic、defer 和 recover

Panic、defer 和 recover 用于处理复杂错误。函数中最后返回的变量用作错误。以下代码片段是示例:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing fmt and errors packages

import(
  "fmt"
  "errors"

)

//First Func method
func FirstFunc(v interface{}) (interface{}, error) {
  var ok bool

  if !ok {
    return nil, errors.New("false error")
  }
  return v, nil
}

//SecondFunc method
func SecondFunc() {
  defer func() {
    var err interface{}
    if err = recover(); err != nil {
      fmt.Println("recovering error ", err)
    }
  }()
  var v interface{}
  v = struct{}{}
  var err error
  if _, err = FirstFunc(v); err != nil {
    panic(err)
  }

  fmt.Println("The error never happen")
}

//main method
func main() {
  SecondFunc()
  fmt.Println("The execution ended")
}

执行以下命令以运行前面的代码片段:

go run handling_error.go

输出如下:

以下链接包含一些关于编写 Go 代码的有用技巧和技术:golang.org/doc/effective_go.html

使用行号进行日志记录

在记录日志时,你可以使用行号和方法名称进行记录。以下代码片段展示了如何使用行号和方法名称来执行日志记录:

//main package has examples shown
//in Go Data Structures and algorithms book
package main

//importing path, runtime, fmt, log and time packages

import(
  "path"
  "runtime"
  "fmt"
  "log"
  "time"
)

//checkPoint method
func checkPoint() string {
    pc, file, line, _ := runtime.Caller(1)
    return fmt.Sprintf("\03331m%v %s %s %d\x1b[0m", time.Now(),
      runtime.FuncForPC(pc).Name(), path.Base(file), line)
}

//method1
func method1(){
  fmt.Println(checkPoint())
}

//main method
func main() {

  log.SetFlags(log.LstdFlags | log.Lshortfile)

  log.Println("logging the time and flags")

  method1()

}

执行以下命令以运行前面的代码片段:

go run log_linenumber.go

输出如下:

![

Go 工具使用

可以使用以下命令调用 Go 工具编译器:

go build -gcflags="-S -N" 

列表选项命令语法如下:

go build -x 

要测试竞争条件,可以使用以下命令:

go test -race 

通过名称运行测试方法可以使用以下语法:

go test -run=method1 

要更新你的 Go 版本,可以使用以下命令:

go get -u 

可以使用以下命令进行复制:

go get -d 

要获取深度,可以使用以下命令:

go get -t 

要获取软件列表,可以使用以下命令:

go list -f 

Go 环境变量

可以使用以下命令将GOROOT变量配置为环境变量:

export GOROOT=/opt/go1.7.1

可以使用以下命令将PATH变量配置为环境变量:

export PATH=$GOROOT/bin:$PATH

可以使用以下命令将GOPATH变量配置为环境变量:

export GOPATH=$HOME/go

可以使用以下命令在PATH变量中配置GOPATH变量:

export PATH=$GOPATH/bin:$PATH

测试表

测试是由测试表驱动的。以下代码片段展示了如何使用测试表:

//main package has examples shown
// in Go Data Structures and algorithms book
package main

// importing testing packages

import (
  "testing"
)

func TestAddition(test *testing.T) {

  cases := []struct{ integer1 , integer2 , resultSum int }{
    {1, 1, 2},
    {1, -1, 0},
    {1, 0, 1},
    {0, 0, 0},
    {3, 2, 1},
  }

  for _, cas := range cases {
    var sum int
    var expected int
    sum = cas.integer1 + cas.integer2
    expected = cas.resultSum
    if sum != expected {
      test.Errorf("%d + %d = %d, expected %d", cas.integer1, cas.integer2, sum, expected)
    }
  }

}

执行以下命令以运行前面的代码片段:

go test -run TestAddition -v

输出如下:

图片

导入包

您可以使用以下语句导入包。在此,我们展示了三种不同的语法选项:

import "fmt"
import ft "fmt"
import . "fmt"
posted @ 2025-09-06 13:43  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报