数据结构(算法)的介绍
数据结构的介绍
1)数据结构是一门研究算法的学科,只从有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码。
2)要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
3)程序 = 数据结构 + 算法
数据结构和算法的关系
算法是程序的灵魂,为什么有些网站能够在高并发,和海量吞吐情况下依然坚如磐石,大家可能会说: 网站使用了服务器群集技术、数据库读写分离和缓存技术(比如 Redis 等),那如果我再深入的问一句,这些优化技术又是怎样被那些天才的技术高手设计出来的呢?
大家请思考一个问题,是什么让不同的人写出的代码从功能看是一样的,但从效率上却有天壤之别, 拿在公司工作的实际经历来说, 我是做服务器的,环境是 UNIX,功能是要支持上千万人同时在线, 并保证数据传输的稳定, 在服务器上线前,做内测,一切 OK,可上线后,服务器就支撑不住了, 公司的 CTO 对我的代码进行优化,再次上线,坚如磐石。那一瞬间,我认识到程序是有灵魂的,就是算法。如果你不想永远都是代码工人,那就花时间来研究下算法吧!
本章着重讲解算法的基石-数据结构。
看几个实际编程中遇到的问题
稀疏 sparsearray 数组
先看一个实际的需求
编写的五子棋程序中,有存盘退出和续上盘的功能
分析按照原始的方式来的二维数组的问题
因为该二维数组的很多值是默认值 0, 因此记录了很多没有意义的数据
基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
1)记录数组一共有几行几列,有多少个不同的值
2)思想:把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
稀疏数组举例说明
应用实例
1)使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
2)把稀疏数组存盘,并且可以从新恢复原来的二维数组数
3)整体思路分析
package main import ( "fmt" ) type ValNode struct { row int col int val int } func main() { //1. 先创建一个原始数组 var chessMap [11][11]int chessMap[1][2] = 1 //黑子 chessMap[2][3] = 2 //蓝子 //2. 输出看看原始的数组 for _, v := range chessMap { for _, v2 := range v { fmt.Printf("%d ", v2) } fmt.Println() } //3. 转成稀疏数组。想-> 算法 // 思 路 //(1). 遍历 chessMap, 如果我们发现有一个元素的值不为 0,创建一个 node 结构体 //(2). 将其放入到对应的切片即可 var sparseArr []ValNode //标准的一个稀疏数组应该还有一个 记录元素的二维数组的规模(行和列,默认值) //创建一个 ValNode 值结点 valNode := ValNode{ row : 11, col : 11, val : 0, } sparseArr = append(sparseArr, valNode) for i, v := range chessMap { for j, v2 := range v { if v2 != 0 { //创建一个 ValNode 值结点 valNode := ValNode{ row : i, col : j, val : v2, } sparseArr = append(sparseArr, valNode) } } } //输出稀疏数组 fmt.Println("当前的稀疏数组是:::::") for i, valNode := range sparseArr { fmt.Printf("%d: %d %d %d ", i, valNode.row, valNode.col, valNode.val) } //将这个稀疏数组,存盘 d:/chessmap.data //如何恢复原始的数组 //1. 打开这个 d:/chessmap.data => 恢复原始数组. //2. 这里使用稀疏数组恢复 // 先创建一个原始数组 var chessMap2 [11][11]int // 遍历 sparseArr [遍历文件每一行] for i, valNode := range sparseArr { if i != 0 { //跳过第一行记录值 chessMap2[valNode.row][valNode.col] = valNode.val } } // 看 看 chessMap2 是 不 是 恢 复 . fmt.Println("恢复后的原始数据 ") for _, v := range chessMap2 { for _, v2 := range v { fmt.Printf("%d ", v2) } fmt.Println() } }
队列(queue)
队列的应用场景
队列介绍
队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
示意图:(使用数组模拟队列示意图)
数组模拟队列
先完成一个非环形的队列(数组来实现)
思路分析
package main import ( "fmt" "os" "errors" ) //使用一个结构体管理队列 type Queue struct { maxSize int array [5]int // 数组=>模拟队列 front int // 表示指向队列首 rear int // 表示指向队列的尾部 } //添加数据到队列 func (this *Queue) AddQueue(val int) (err error) { //先判断队列是否已满 if this.rear == this.maxSize - 1 { //重要重要的提示; rear 是队列尾部(含最后元素) return errors.New("queue full") } this.rear++ //rear 后 移 this.array[this.rear] = val return } //从队列中取出数据 func (this *Queue) GetQueue() (val int, err error) { //先判断队列是否为空 if this.rear == this.front { //队空 return -1, errors.New("queue empty") } this.front++ val = this.array[this.front] return val ,err } //显示队列, 找到队首,然后到遍历到队尾 func (this *Queue) ShowQueue() { fmt.Println("队列当前的情况是:") //this.front 不包含队首的元素 for i := this.front + 1; i <= this.rear; i++ { fmt.Printf("array[%d]=%d ", i, this.array[i]) } fmt.Println() } //编写一个主函数测试,测试 func main() { //先创建一个队列 queue := &Queue{ maxSize : 5, front : -1, rear : -1, } var key string var val int for { fmt.Println("1. 输入 add 表示添加数据到队列") fmt.Println("2. 输入 get 表示从队列获取数据") fmt.Println("3. 输 入 show 表 示 显 示 队 列 ") fmt.Println("4. 输入 exit 表示显示队列") fmt.Scanln(&key) switch key { case "add": fmt.Println("输入你要入队列数") fmt.Scanln(&val) err := queue.AddQueue(val) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("加入队列 ok") } case "get": val, err := queue.GetQueue() if err != nil { fmt.Println(err.Error()) } else { fmt.Println("从队列中取出了一个数=", val) } case "show": queue.ShowQueue() case "exit": os.Exit(0) } } }
对上面代码的小结和说明:
1)上面代码实现了基本队列结构,但是没有有效的利用数组空间
2)请思考,如何使用数组 实现一个环形的队列
数组模拟环形队列
分析思路:
1)什么时候表示队列满 (tail + 1) % maxSize = hed
2)tail == head 表示空
3)初始化时, tail = 0 head = 0
4)怎么统计该队列有多少个元素 (tail + maxSize - head ) % maxSize
package main import ( "fmt" "errors" "os" ) //使用一个结构体管理环形队列 type CircleQueue struct { maxSize int // 4 array [5]int // 数 组 head int //指向队列队首 0 tail int //指向队尾 0 } //如队列 AddQueue(push) GetQueue(pop) //入队列 func (this *CircleQueue) Push(val int) (err error) { if this.IsFull() { return errors.New("queue full") } //分析出 this.tail 在队列尾部,但是包含最后的元素 this.array[this.tail] = val //把值给尾部 this.tail = (this.tail + 1) % this.maxSize return } //出队列 func (this *CircleQueue) Pop() (val int, err error) { if this.IsEmpty() { return 0, errors.New("queue empty") } //取出,head 是指向队首,并且含队首元素 val = this.array[this.head] this.head = (this.head + 1) % this.maxSize return } //显示队列 func (this *CircleQueue) ListQueue() { fmt.Println("环形队列情况如下:") //取出当前队列有多少个元素 size := this.Size() if size == 0 { fmt.Println("队列为空") } //设计一个辅助的变量,指向 head tempHead := this.head for i := 0; i < size; i++ { fmt.Printf("arr[%d]=%d ", tempHead, this.array[tempHead]) tempHead = (tempHead + 1) % this.maxSize } fmt.Println() } //判断环形队列为满 func (this *CircleQueue) IsFull() bool { return (this.tail + 1) % this.maxSize == this.head } //判断环形队列是空 func (this *CircleQueue) IsEmpty() bool { return this.tail == this.head } //取出环形队列有多少个元素 func (this *CircleQueue) Size() int { //这是一个关键的算法. return (this.tail + this.maxSize - this.head ) % this.maxSize } func main() { //初始化一个环形队列 queue := &CircleQueue{ maxSize : 5, head : 0, tail : 0, } var key string var val int for { fmt.Println("1. 输入 add 表示添加数据到队列") fmt.Println("2. 输入 get 表示从队列获取数据") fmt.Println("3. 输 入 show 表 示 显 示 队 列 ") fmt.Println("4. 输入 exit 表示显示队列") fmt.Scanln(&key) switch key { case "add": fmt.Println("输入你要入队列数") fmt.Scanln(&val) err := queue.Push(val) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("加入队列 ok") } case "get": val, err := queue.Pop() if err != nil { fmt.Println(err.Error()) } else { fmt.Println("从队列中取出了一个数=", val) } case "show": queue.ListQueue() case "exit": os.Exit(0) } } }
链表
链表介绍
链表是有序的列表,但是它在内存中是存储如下
单链表的介绍
单链表的示意图:
说明:一般来说,为了比较好的对单链表进行增删改查的操作,我们都会给他设置一个头结点, 头结点的作用主要是用来标识链表头, 本身这个结点不存放数据。
单链表的应用实例
案例的说明:
使用带 head 头的单向链表实现 –水浒英雄排行榜管理 完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成
第一种方法在添加英雄时,直接添加到链表的尾部
代码实现:
package main import ( "fmt" ) //定义一个 HeroNode type HeroNode struct { no int name string nickname string next *HeroNode //这个表示指向下一个结点 } //给链表插入一个结点 //编写第一种插入方法,在单链表的最后加入.[简单] func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode) { //思路 //1. 先找到该链表的最后这个结点 //2. 创建一个辅助结点[跑龙套, 帮忙] temp := head for { if temp.next == nil { //表示找到最后 break } temp = temp.next // 让 temp 不断的指向下一个结点 } //3. 将 newHeroNode 加入到链表的最后 temp.next = newHeroNode } //给链表插入一个结点 //编写第 2 种插入方法,根据 no 的编号从小到大插入..【实用】 func InsertHeroNode2(head *HeroNode, newHeroNode *HeroNode) { //思路 //1. 找到适当的结点 //2. 创建一个辅助结点[跑龙套, 帮忙] temp := head flag := true //让插入的结点的 no,和 temp 的下一个结点的 no 比较 for { if temp.next == nil {//说明到链表的最后 break } else if temp.next.no >= newHeroNode.no { //说明 newHeroNode 就应该插入到 temp 后面 break } else if temp.next.no == newHeroNode.no { //说明我们额链表中已经有这个 no,就不然插入. flag = false break } temp = temp.next } if !flag { fmt.Println("对不起,已经存在 no=", newHeroNode.no) return } else { newHeroNode.next = temp.next temp.next = newHeroNode } } //显示链表的所有结点信息 func ListHeroNode(head *HeroNode) { //1. 创建一个辅助结点[跑龙套, 帮忙] temp := head // 先判断该链表是不是一个空的链表 if temp.next == nil { fmt.Println("空空如也。。。。") return } //2. 遍历这个链表 for { fmt.Printf("[%d , %s , %s]==>", temp.next.no, temp.next.name, temp.next.nickname) //判断是否链表后 temp = temp.next if temp.next == nil { break } } } func main() { //1. 先创建一个头结点, head := &HeroNode{} //2. 创建一个新的 HeroNode hero1 := &HeroNode{ no : 1, name : " 宋 江 ", nickname : "及时雨", } hero2 := &HeroNode{ no : 2, name : "卢俊义", nickname : "玉麒麟", } hero3 := &HeroNode{ no : 3, name : "林冲", nickname : "豹子头", } hero4 := &HeroNode{ no : 3, name : "吴用", nickname : "智多星", } //3. 加 入 InsertHeroNode2(head, hero3) InsertHeroNode2(head, hero1) InsertHeroNode2(head, hero2) InsertHeroNode2(head, hero4) // 4. 显 示 ListHeroNode(head) }
删除节点
双向链表的应用实例
示意图
代码实现
package main import ( "fmt" ) // 定义一个 HeroNode type HeroNode struct { no int name string nickname string pre *HeroNode //这个表示指向前一个结点 next *HeroNode //这个表示指向下一个结点 } //给双向链表插入一个结点 //编写第一种插入方法,在单链表的最后加入.[简单] func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode) { //思路 //1. 先找到该链表的最后这个结点 //2. 创建一个辅助结点[跑龙套, 帮忙] temp := head for { if temp.next == nil { //表示找到最后 break } temp = temp.next // 让 temp 不断的指向下一个结点 } //3. 将 newHeroNode 加入到链表的最后 temp.next = newHeroNode newHeroNode.pre = temp } //给双向链表插入一个结点 //编写第 2 种插入方法,根据 no 的编号从小到大插入..【实用】 func InsertHeroNode2(head *HeroNode, newHeroNode *HeroNode) { //思路 //1. 找到适当的结点 //2. 创建一个辅助结点[跑龙套, 帮忙] temp := head flag := true //让插入的结点的 no,和 temp 的下一个结点的 no 比较 for { if temp.next == nil {//说明到链表的最后 break } else if temp.next.no >= newHeroNode.no { //说明 newHeroNode 就应该插入到 temp 后面 break } else if temp.next.no == newHeroNode.no { //说明我们额链表中已经有这个 no,就不然插入. flag = false break } temp = temp.next } if !flag { fmt.Println("对不起,已经存在 no=", newHeroNode.no) return } else { newHeroNode.next = temp.next //ok newHeroNode.pre = temp//ok if temp.next != nil { temp.next.pre = newHeroNode //ok } temp.next = newHeroNode //ok } } //删除一个结点[双向链表删除一个结点] func DelHerNode(head *HeroNode, id int) { temp := head flag := false //找到要删除结点的 no,和 temp 的下一个结点的 no 比较 for { if temp.next == nil {//说明到链表的最后 break } else if temp.next.no == id { //说明我们找到了. flag = true break } temp = temp.next } if flag {//找到, 删除 temp.next = temp.next.next //ok if temp.next != nil { temp.next.pre = temp } } else { fmt.Println("sorry, 要删除的 id 不存在") } } //显示链表的所有结点信息 //这里仍然使用单向的链表显示方式 func ListHeroNode(head *HeroNode) { //1. 创建一个辅助结点[跑龙套, 帮忙] temp := head // 先判断该链表是不是一个空的链表 if temp.next == nil { fmt.Println("空空如也。。。。") return } //2. 遍历这个链表 for { fmt.Printf("[%d , %s , %s]==>", temp.next.no, temp.next.name, temp.next.nickname) //判断是否链表后 temp = temp.next if temp.next == nil { break } } } func ListHeroNode2(head *HeroNode) { //1. 创建一个辅助结点[跑龙套, 帮忙] temp := head // 先判断该链表是不是一个空的链表 if temp.next == nil { fmt.Println("空空如也。。。。") return } //2. 让 temp 定位到双向链表的最后结点 for { if temp.next == nil { break } temp = temp.next } //2. 遍历这个链表 for { fmt.Printf("[%d , %s , %s]==>", temp.no, temp.name, temp.nickname) //判断是否链表头 temp = temp.pre if temp.pre == nil { break } } } func main() { //1. 先创建一个头结点, head := &HeroNode{} //2. 创建一个新的 HeroNode hero1 := &HeroNode{ no : 1, name : " 宋 江 ", nickname : "及时雨", } hero2 := &HeroNode{ no : 2, name : "卢俊义", nickname : "玉麒麟", } hero3 := &HeroNode{ no : 3, name : " 林 冲 ", nickname : "豹子头", } InsertHeroNode(head, hero1) InsertHeroNode(head, hero2) InsertHeroNode(head, hero3) ListHeroNode(head) fmt.Println(" 逆 序 打 印 ") ListHeroNode2(head) }
单向环形链表的应用场景
环形单向链表介绍
环形的单向链表的案例
完成对单向环形链表的添加结点,删除结点和显示
package main import ( "fmt" ) //定义猫的结构体结点 type CatNode struct { no int //猫猫的编号 name string next *CatNode } func InsertCatNode(head *CatNode, newCatNode *CatNode) { //判断是不是添加第一只猫 if head.next == nil { head.no = newCatNode.no head.name = newCatNode.name head.next = head //构成一个环形 fmt.Println(newCatNode, "加入到环形的链表") return } //定义一个临时变量,帮忙,找到环形的最后结点 temp := head for { if temp.next == head { break } temp = temp.next } //加入到链表中 temp.next = newCatNode newCatNode.next = head } //输出这个环形的链表 func ListCircleLink(head *CatNode) { fmt.Println("环形链表的情况如下:") temp := head if temp.next == nil { fmt.Println("空空如也的环形链表...") return } for { fmt.Printf("猫的信息为=[id=%d name=%s] -> ", temp.no, temp.name) if temp.next == head { break } temp = temp.next } } //删除一只猫 func DelCatNode(head *CatNode, id int) *CatNode { temp := head helper := head //空链表 if temp.next == nil { fmt.Println("这是一个空的环形链表,不能删除") return head } //如果只有一个结点 if temp.next == head { //只有一个结点 if temp.no == id { temp.next = nil } return head } //将 helper 定位到链表最后 for { if helper.next == head { break } helper = helper.next } //如果有两个包含两个以上结点 flag := true for { if temp.next == head { //如果到这来,说明我比较到最后一个【最后一个还没比较】 break } if temp.no ==id { if temp == head { //说明删除的是头结点 head = head.next } //恭喜找到., 我们也可以在直接删除 helper.next = temp.next fmt.Printf("猫猫=%d ", id) flag = false break } temp = temp.next //移动 【比较】 helper = helper.next //移动 【一旦找到要删除的结点 helper】 } //这里还有比较一次 if flag { //如果 flag 为真,则我们上面没有删除 if temp.no == id { helper.next = temp.next fmt.Printf("猫猫=%d ", id) }else { fmt.Printf("对不起,没有 no=%d ", id) } } return head } func main() { //这里我们初始化一个环形链表的头结点 head := &CatNode{} //创建一只猫 cat1 := &CatNode{ no : 1, name : "tom", } cat2 := &CatNode{ no : 2, name : "tom2", } cat3 := &CatNode{ no : 3, name : "tom3", } InsertCatNode(head, cat1) InsertCatNode(head, cat2) InsertCatNode(head, cat3) ListCircleLink(head) head = DelCatNode(head, 30) fmt.Println() fmt.Println() fmt.Println() ListCircleLink(head) }
环形单向链表的应用实例
Josephu 问 题
Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推, 直到所有人出列为止,由此产生一个出队编号的序列。
提示
用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
示意图
package main import ( "fmt" ) //小孩的结构体 type Boy struct { No int // 编 号 Next *Boy // 指向下一个小孩的指针[默认值是 nil] } // 编写一个函数,构成单向的环形链表 // num :表示小孩的个数 // *Boy : 返回该环形的链表的第一个小孩的指针 func AddBoy(num int) *Boy { first := &Boy{} //空结点 curBoy := &Boy{} //空结点 //判断 if num < 1 { fmt.Println("num 的值不对") return first } //循环的构建这个环形链表 for i := 1; i <= num; i++ { boy := &Boy{ No : i, } //分析构成循环链表,需要一个辅助指针[帮忙的] //1. 因为第一个小孩比较特殊 if i == 1 { //第一个小孩first = boy //不要动 curBoy = boy curBoy.Next = first // } else { curBoy.Next = boy curBoy = boy curBoy.Next = first //构造环形链表 } } return first } //显示单向的环形链表[遍历] func ShowBoy(first *Boy) { //处理一下如果环形链表为空 if first.Next == nil { fmt.Println("链表为空,没有小孩...") return } //创建一个指针,帮助遍历.[说明至少有一个小孩] curBoy := first for { fmt.Printf("小孩编号=%d ->", curBoy.No) //退出的条件?curBoy.Next == first if curBoy.Next == first { break } //curBoy 移动到下一个 curBoy = curBoy.Next } } /* 设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n) 的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数, 数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列 */ //分析思路 //1. 编写一个函数,PlayGame(first *Boy, startNo int, countNum int) //2. 最后我们使用一个算法,按照要求,在环形链表中留下最后一个人 func PlayGame(first *Boy, startNo int, countNum int) { //1. 空的链表我们单独的处理 if first.Next == nil { fmt.Println("空的链表,没有小孩") return } //留一个,判断 startNO <= 小孩的总数 //2. 需要定义辅助指针,帮助我们删除小孩 tail := first //3. 让 tail 执行环形链表的最后一个小孩,这个非常的重要 //因为 tail 在删除小孩时需要使用到. for { if tail.Next == first { //说明 tail 到了最后的小孩 break } tail = tail.Next } //4. 让 first 移动到 startNo [后面我们删除小孩,就以 first 为准] for i := 1; i <= startNo - 1; i++ { first = first.Next tail = tail.Next } fmt.Println() //5. 开始数 countNum, 然后就删除 first 指向的小孩 for { //开始数 countNum-1 次 for i := 1; i <= countNum -1; i++ { first = first.Next tail = tail.Next } fmt.Printf("小孩编号为%d 出圈 ", first.No) //删除 first 执行的小孩 first = first.Next tail.Next = first //判断如果 tail == first, 圈子中只有一个小孩. if tail == first { break } } fmt.Printf("小孩小孩编号为%d 出圈 ", first.No) } func main() { first := AddBoy(500) //显示ShowBoy(first) PlayGame(first, 20, 31) }
排序
排序的介绍
排序是将一组数据,依指定的顺序进行排列的过程, 常见的排序:
1)冒泡排序
2)选择排序
3)插入排序
4)快速排序
冒泡排序
选择排序基本介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,经过和其他元素重整,再依原则交换位置后达到排序的目的。
选择排序思想:
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从R[0]~R[n-1]中选取最小值,与 R[0]交换,第二次从 R[1]~R[n-1]中选取最小值,与 R[1]交换,第三次从 R[2]~R[n-1]中选取最小值,与 R[2]交换,…,第 i 次从 R[i-1]~R[n-1]中选取最小值,与 R[i-1]交换,…, 第 n-1 次从R[n-2]~R[n-1]中选取最小值,与 R[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。
示意图
代码实现
插入排序法介绍:
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序法思想:
插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表, 开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
插入排序的示意图
实例
代码实现
快速排序法介绍
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序法示意图
实例
package main import ( "fmt" ) //快速排序 //说明 //1. left 表示 数组左边的下标 //2. right 表示数组右边的下标 //3 array 表示要排序的数组 func QuickSort(left int, right int, array *[9]int) { l := left r := right // pivot 是中轴, 支点 pivot := array[(left + right) / 2] temp := 0 //for 循环的目标是将比 pivot 小的数放到 左边 // 比 pivot 大的数放到 右边 for ; l < r; { //从 pivot 的左边找到大于等于 pivot 的值 for ; array[l] < pivot; { l++ } //从 pivot 的右边边找到小于等于 pivot 的值 for ; array[r] > pivot; { r-- } // 1 >= r 表明本次分解任务完成, break if l >= r { break } //交换 temp = array[l] array[l] = array[r] array[r] = temp //优化 if array[l]== pivot { r-- } if array[r]== pivot { l++ } } // 如果 1== r, 再移动下 if l == r { l++ r-- } // 向左递归 if left < r { QuickSort(left, r, array) } // 向右递归 if right > l { QuickSort(l, right, array) } } func main() { arr := [9]int {-9,78,0,23,-567,70, 123, 90, -23} fmt.Println("初始", arr) //调用快速排序 QuickSort(0, len(arr) - 1, &arr) fmt.Println("main..") fmt.Println(arr) }
栈
看一个实际需求
栈的介绍
有些程序员也把栈称为堆栈, 即栈和堆栈是同一个概念
1)栈的英文为(stack)
2)栈是一个先入后出(FILO-First In Last Out)的有序列表。
3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
4)根据堆栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
栈的入栈和出栈的示意图
栈的应用场景
1)子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2)处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
3)表达式的转换与求值。
4)二叉树的遍历。
5)图形的深度优先(depth 一 first)搜索法。
栈的案例
代码实现
栈实现综合计算器
分析了实现的思路
代码实现
package main import ( "fmt" "errors" "strconv" ) //使用数组来模拟一个栈的使用 type Stack struct { MaxTop int // 表示我们栈最大可以存放数个数 Top int // 表示栈顶, 因为栈顶固定,因此我们直接使用 Top arr [20]int // 数组模拟栈 } //入栈 func (this *Stack) Push(val int) (err error) { //先判断栈是否满了 if this.Top == this.MaxTop - 1 { fmt.Println("stack full") return errors.New("stack full") } this.Top++ //放入数据 this.arr[this.Top] = val return } //出栈 func (this *Stack) Pop() (val int, err error) { //判断栈是否空 if this.Top == -1 { fmt.Println("stack empty!") return 0, errors.New("stack empty") } //先取值,再 this.Top-- val = this.arr[this.Top] this.Top-- return val, nil } //遍历栈,注意需要从栈顶开始遍历 func (this *Stack) List() { //先判断栈是否为空 if this.Top == -1 { fmt.Println("stack empty") return } fmt.Println("栈的情况如下:") for i := this.Top; i >= 0; i-- { fmt.Printf("arr[%d]=%d ", i, this.arr[i]) } } //判断一个字符是不是一个运算符[+, - , * , /] func (this *Stack) IsOper(val int) bool { if val == 42 || val == 43 || val == 45 || val == 47 { return true } else { return false } } //运算的方法 func (this *Stack) Cal(num1 int, num2 int, oper int) int{ res := 0 switch oper { case 42 : res = num2 * num1 case 43 : res = num2 + num1 case 45 : res = num2 - num1 case 47 : res = num2 / num1 default : fmt.Println("运算符错误.") } return res } //编写一个方法,返回某个运算符的优先级[程序员定义] //[* / => 1 + - => 0] func (this *Stack) Priority(oper int) int { res := 0 if oper == 42 || oper == 47 { res = 1 } else if oper == 43 || oper == 45 { res = 0 } return res } func main() { //数栈 numStack := &Stack{ MaxTop : 20, Top : -1, } //符号栈 operStack := &Stack{ MaxTop : 20, Top : -1, } exp := "30+30*6-4-6" //定义一个 index ,帮助扫描 exp index := 0 //为了配合运算,我们定义需要的变量num1 := 0 num2 := 0 oper := 0 result := 0 keepNum := "" for { //这里我们需要增加一个逻辑, //处理多位数的问题 ch := exp[index:index+1] // 字符串. //ch ==>"+" ===> 43 temp := int([]byte(ch)[0]) // 就是字符对应的 ASCiI 码 if operStack.IsOper(temp) { // 说明是符号 //如果 operStack 是一个空栈, 直接入栈 if operStack.Top == -1 { //空栈 operStack.Push(temp) }else { //如果发现 opertStack 栈顶的运算符的优先级大于等于当前准备入栈的运算符的优先级 //,就从符号栈 pop 出,并从数栈也 pop 两个数,进行运算,运算后的结果再重新入栈 //到数栈, 当前符号再入符号栈 if operStack.Priority(operStack.arr[operStack.Top]) >= operStack.Priority(temp) { num1, _ = numStack.Pop() num2, _ = numStack.Pop() oper, _ = operStack.Pop() result = operStack.Cal(num1,num2, oper) //将计算结果重新入数栈 numStack.Push(result) //当前的符号压入符号栈 operStack.Push(temp) }else { operStack.Push(temp) } } } else { //说明是数 //处理多位数的思路 //1.定义一个变量 keepNum string, 做拼接 keepNum += ch //2.每次要向 index 的后面字符测试一下,看看是不是运算符,然后处理 //如果已经到表达最后,直接将 keepNum if index == len(exp) - 1 { val, _ := strconv.ParseInt(keepNum, 10, 64) numStack.Push(int(val)) } else { //向 index 后面测试看看是不是运算符 [index] if operStack.IsOper(int([]byte(exp[index+1:index+2])[0])) { val, _ := strconv.ParseInt(keepNum, 10, 64) numStack.Push(int(val)) keepNum = "" } } } //继续扫描 //先判断 index 是否已经扫描到计算表达式的最后 if index + 1 == len(exp) { break } index++ } //如果扫描表达式 完毕,依次从符号栈取出符号,然后从数栈取出两个数, //运算后的结果,入数栈,直到符号栈为空 for { if operStack.Top == -1 { break //退出条件 } num1, _ = numStack.Pop() num2, _ = numStack.Pop() oper, _ = operStack.Pop() result = operStack.Cal(num1,num2, oper) //将计算结果重新入数栈 numStack.Push(result) } //如果我们的算法没有问题,表达式也是正确的,则结果就是 numStack 最后数 res, _ := numStack.Pop() fmt.Printf("表达式%s = %v", exp, res) }
递归
递归的一个应用场景[迷宫问题]
递归的概念
简单的说: 第归就是函数/方法自己调用自己,每次调用时传入不同的变量.第归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
递归快速入门
我列举两个小案例,来帮助大家理解递归,递归在讲函数时已经讲过(当时讲的相对比较简单),这里在给大家回顾一下递归调用机制
1)打印问题
2)阶乘问题
3)快速入门的示意图
递归用于解决什么样的问题
1)各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
2)将用栈解决的问题-->第归代码比较简洁
递归需要遵守的重要原则
1)执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
2)函数的局部变量是独立的,不会相互影响, 如果希望各个函数栈使用同一个数据,使用引用传递
3)递归必须向退出递归的条件逼近【程序员自己必须分析】,否则就是无限递归,死龟了:)
4)当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
举一个比较综合的案例,迷宫问题
package main import ( "fmt" ) //编写一个函数,完成老鼠找路 //myMap *[8][7]int:地图,保证是同一个地图,使用引用 //i,j 表示对地图的哪个点进行测试 func SetWay(myMap *[8][7]int, i int, j int) bool { //分析出什么情况下,就找到出路 //myMap[6][5] == 2 if myMap[6][5] == 2 { return true } else { //说明要继续找 if myMap[i][j] == 0 { //如果这个点是可以探测 //假设这个点是可以通, 但是需要探测 上下左右 //换一个策略 下右上左 myMap[i][j] = 2 if SetWay(myMap, i + 1, j) { //下 return true } else if SetWay(myMap, i , j + 1) { //右 return true } else if SetWay(myMap, i - 1, j) { //上 return true } else if SetWay(myMap, i , j - 1) { //左 return true } else { // 死 路 myMap[i][j] = 3 return false } } else { // 说明这个点不能探测,为 1,是强 return false } } } func main() { //先创建一个二维数组,模拟迷宫 //规则 //1. 如果元素的值为 1 ,就是墙 //2. 如果元素的值为 0, 是没有走过的点 //3. 如果元素的值为 2, 是一个通路 //4. 如果元素的值为 3, 是走过的点,但是走不通 var myMap [8][7]int //先把地图的最上和最下设置为 1 for i := 0 ; i < 7 ; i++ { myMap[0][i] = 1 myMap[7][i] = 1 } //先把地图的最左和最右设置为 1 for i := 0 ; i < 8 ; i++ { myMap[i][0] = 1 myMap[i][6] = 1 } myMap[3][1] = 1 myMap[3][2] = 1 myMap[1][2] = 1 myMap[2][2] = 1 //输出地图 for i := 0; i < 8; i++ { for j := 0; j < 7; j++ { fmt.Print(myMap[i][j], " ") } fmt.Println() } //使用测试 SetWay(&myMap, 1, 1) fmt.Println("探测完毕 ") //输出地图 for i := 0; i < 8; i++ { for j := 0; j < 7; j++ { fmt.Print(myMap[i][j], " ") } fmt.Println() } }
哈希表(散列)
哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
使用hashtable 来实现一个雇员的管理系统[增删改查]
应用实例 google 公司的一个上机题:
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的 id 时,要求查找到该员工的 所有信息.
要求:
1)不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
2)添加时,保证按照雇员的 id 从低到高插入
思路分析
1)使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
2)思路分析并画出示意图
代码实现