《大话数据结构》golang代码练习

1.求1+2+3+......+100  

循环累加

package main

import "fmt"

func main() {
	sum, n := 0, 100
	for i:=1;i<=n;i++ {
    	sum = sum + i
	}
   fmt.Printf("%d", sum)
}

高斯算法求和

相当于等差数列求和

package main

import "fmt"

func main() {
	sum, n := 0, 100
	sum = (1 + n) * n / 2
	fmt.Printf("%d", sum)
}

2.7 算法效率的度量方法

事后统计方法

事前分析估算方法

package main

import "fmt"

func main() {
	x, sum, n := 0, 0, 100 //执行1次
	for i := 1; i <= n; i++ {
		for j := 1; j <= n; j++ {
			x++            //执行1次
			sum = sum + x
		}
	}
	fmt.Printf("%d", sum)  //执行1次
}

2.8 函数的渐近增长

2.9 算法时间复杂度

我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n^2)。O(1)叫常数阶,O(1)叫线性阶,O(n^2)叫平方阶。

2.9.2 推导大O阶方法

2.9.3 常数阶

2.9.4 线性阶

2.9.5 对数阶

下面这对代码,时间复杂度是多少?

package main

import "fmt"

func main() {
    count, n := 1, 100
    for count < n {
        count = count * 2
        //时间复杂度为O(1)的程序步骤序列
    }
}

2^x=n,x=log2 n真数,所以这个循环的时间复杂度是O(logn)。

2.9.6 平方阶

2.10 常见的时间复杂度

2.11 最坏情况和平均情况

2.12 算法空间时间复杂度

要判断某某年是不是闰年

判断一个年份是否是闰年,需要满足下面条件之一:
年份能被4整除,但不能被100整除;
能被400整除

package main

import (
    "fmt"
)

func main() {
    var year int
    fmt.println("请输入年份:")
    fmt.Scanln(&year)
    if (year%4==0 &&year%100!=0) || year%400==0 {
        fmt.println(year, "是闰年。")
    } else {
        fmt.println(year, "不是闰年。")
    }
}

2.13 总结回顾

第3章 线性表

要实现两个线性表集合A和B的并集操作

//两个集合取并集

package main

import "fmt"

//思想:
//运用map,统计nums1中值出现的次数,即map[值]次数
//遍历nums2中的值,查看值是否在map中的出现

func union(num1, num2 []string) []string {
    m := make(map[string]int)
    for _, v := range num1 {
        m[v]++
    }
    fmt.Println(m)

    for _, v := range num2 {
//        times, bool := m[v]
//		if bool != true {
//			fmt.Println("不存在")
//		}
        times, _ := m[v]
        fmt.Printf("v=%s,times=%d\n", v, times)
        if times == 0 {
            num1 = append(num1, v)
        }            
    }
    return num1
}

func main() {
    num1 := []string{"3", "4", "1"}
    num2 := []string{"2", "1", "3"}
    fmt.Println(union(num1,num2))
}
package model


import (
    "sort"
    "sync"
)

type Set struct {
    sync.RWMutex
    m map[int]bool
}

// 新建集合对象
func New(items ...int) *Set {
    s := &Set{
        m: make(map[int]bool, len(items)),
    }
    s.Add(items...)
    return s
}

// 添加元素
func (s *Set) Add(items ...int) {
    s.Lock()
    defer s.Unlock()
    for _, v := range items {
        s.m[v] = true
    }
}

// 删除元素
func (s *Set) Remove(items ...int) {
    s.Lock()
    defer s.Unlock()
    for _, v := range items {
        delete(s.m, v)
    }
}

// 判断元素是否存在
func (s *Set) Has(items ...int) bool {
    s.RLock()
    defer s.RUnlock()
    for _, v := range items {
        if _, ok := s.m[v]; !ok {
            return false
        }
    }
    return true
}

// 元素个数
func (s *Set) Count() int {
    return len(s.m)
}

// 清空集合
func (s *Set) Clear() {
    s.Lock()
    defer s.Unlock()
    s.m = map[int]bool{}
}

// 空集合判断
func (s *Set) Empty() bool {
    return len(s.m) == 0
}

// 无序列表
func (s *Set) List() []int {
    s.RLock()
    defer s.RUnlock()
    list := make([]int, 0, len(s.m))
    for item := range s.m {
        list = append(list, item)
    }
    return list
}

// 排序列表
func (s *Set) SortList() []int {
    s.RLock()
    defer s.RUnlock()
    list := make([]int, 0, len(s.m))
    for item := range s.m {
        list = append(list, item)
    }
    sort.Ints(list)
    return list
}

// 并集
func (s *Set) Union(sets ...*Set) *Set {
    r := New(s.List()...)
    for _, set := range sets {
        for e := range set.m {
            r.m[e] = true
        }
    }
    return r
}

// 差集
func (s *Set) Minus(sets ...*Set) *Set {
    r := New(s.List()...)
    for _, set := range sets {
        for e := range set.m {
            if _, ok := s.m[e]; ok {
                delete(r.m, e)
            }
        }
    }
    return r
}

// 交集
func (s *Set) Intersect(sets ...*Set) *Set {
    r := New(s.List()...)
    for _, set := range sets {
        for e := range s.m {
            if _, ok := set.m[e]; !ok {
                delete(r.m, e)
            }
        }
    }
    return r
}

// 补集
func (s *Set) Complement(full *Set) *Set {
    r := New()
    for e := range full.m {
        if _, ok := s.m[e]; !ok {
            r.Add(e)
        }
    }
    return r
}

3.4 线性表的顺序存储结构

3.5 顺序存储结构的插入与删除

3.5.1 获得元素操作

package main
import (
    "errors"
)
type ElemType int
const MAXSIZE = 20
type SqList struct {
    data [MAXSIZE] ElemType
    length int
}
//获取数据元素
func GetElem(s SqList, i int) (err error, res ElemType) {
    if s.length == 0 || i < 0 || i > s.length {
        err = errors.New("查找失败")
    }
    res = s.data[i] 
    return 
}

3.5.2 插入操作

//插入数据
func ListInsert(s *SqList, i int, e *ElemType) error{
    if s.length == MAXSIZE {
        return errors.New("线性表已满,不能插入数据")
    }
    if i < 0 || i > s.length {
        return errors.New("插入的位置不正确")
    }
    //i位开始后移一位
    //从最后一位向前遍历到第i位
    for j := s.length; j>=i; j-- {
        s.data[j+1] = s.data[j]
    }
    s.data[i] = *e
    s.length++
    return nil
}

3.5.3 删除操作

//删除数据
func ListDelete(s SqList, i int) (err error) {
    if s.length == 0 {
        err = errors.New("线性表为空")
    }
    if i < 0 || i > s.length-1 {
        err = errors.New("删除的位置不正确")
    }
    //从删除元素位置开始向后遍历到最后一个位置,分别将他们向前移动一位
    for j := i; j < s.length-1; j++ {
        s.data[j] = s.data[j+1]
    }
    s.data[s.length-1] = 0 //清除数据
    s.length--
    return 
}

3.5.4 线性表顺序存储结构的优缺点

3.6 线性表的链式存储结构

单链表,可用结构指针来描述

//链表结点
type ListNode struct {
    value int         //定义数据域
    next  *ListNode   //定义指针域
}
//单链表
type LinkList struct {
    // 头结点还是头指针?头结点可以包含指针域,有或者没有数据域
    head  *ListNode
    // 长度
    length int
}

3.7 单链表的读取

获取链表第i个数据的算法思路:

1.声明一个结点p指向链表第一个元素,初始化j从1开始;

2.当j

3.若到链表末尾p为空,则说明第i个元素不存在;

4.否则查找成功,返回结点p的数据。

//初始条件:顺序线性表L已存在,1<=i<=L.length-1
//操作结果:用p返回L中第i个数据元素的值
func (list *LinkList) GetNodeAtIndex(i int) *Node{
    //声明一个结点p,让p指向链表list的头结点
    var p *Node = list.head
    for p!=nil {
        //找到该数据所在结点
        if(i == p.value){
            return p
        } else {
            p = p.next
    }
    return nil
/*	// 索引越界
	if i > list.length-1 || index < 0 {
		return nil
	}

	// 备份头结点
	bak := list.head

	// 向后循环
	for index > -1 {
		bak = bak.pNext
		index--
	}

	return bak
*/
}

3.8 单链表的插入与删除

单链表第i个数据插入结点的算法思路:

1.声明一结点p指向链表第一个结点,初始化j从1开始;

2.当j

3.若到链表末尾p为空,则说明第i个元素不存在;

4.否则查找成功,在系统中生成一个空结点s;

5.将数据元素e赋值给s.value;

6.单链表的插入标准语句s.next=p.next  p.next=s

7.返回成功

func(list *LinkList) insertNodeValueFront(node *LinkNode, i int) bool {
    //备份头结点
    p := list.head
    //循环到末尾,直到找到目标结点
    for p.next !=nil && p.next.value != i {
        p = p.next
    }
    //找到指定元素i
    if p.next.value == i {
        //
        node.next = p.next
        //
        p.next = node
        list.length++
        return true
    }
    return false
}

3.8.2 单链表的删除

单链表第i个数据删除结点的算法思路:

1.声明一结点p指向链表第一个结点,初始化j从1开始;

2.当j

3.若到链表末尾p为空,则说明第i个元素不存在;

4.否则查找成功,将欲删除的结点p->next赋值给q;

5.单链表的删除标准语句p->next=q->next;

6.将q结点中的数据赋值给e,作为返回;

7.释放q结点;

8.返回成功。

实现代码算法:

//操作结果:删除L的第i个数据元素,删除成功返回true,L的长度减1

func(list *LinkList) deleteNode(node *LinkNode, i int) bool {
    //备份头结点
    p := list.head
    //循环到末尾,直到找到目标结点
    for p.next !=nil && p.next.value != i {
        p = p.next
    }
    //找到指定结点/元素i
    if p.next.value == i {
        //
        node = p.next
        //
        p.next = node.next
        list.length--
        return true
    }
    return false
}

单链表插入和删除算法,它们其实都是由两部分组成:第一部分就是遍历查找第i个元素;第二部分就是插入和删除元素。整体复杂度都是O(n)。显然,对于插入和删除数据约频繁的操作,单链表的效率优势就越是明显。

3.9 单链表的整表创建

单链表整表创建的算法思路:

1.声明一结点p和计数器变量i;

2.初始化一空链表L;

3.让L的头节点的指针指向NULL,即建立一个带头结点的单链表;

4.循环:

        生成一新节点赋值给p;

        随机生成一数字赋值给p的数据域p->data;

        将p插入到头结点与前一新结点之间。

实现代码算法:

//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)

golang构造单链表

package main
import "fmt"

type ListNode struct {
    value int
    next  *ListNode
}
type LinkList struct {
    head  *ListNode
}
func main() {
    fmt.Println("链表长度为:",list.getLength())
    fmt.Println("链表是否为空:",list.isEmpty())
    fmt.Print("遍历链表:")
    var q *ListNode=list.head
    for q!=nil{
        fmt.Print(q.value," ")
        q=q.next
    }
    fmt.Println()    
}
func createListHead(list *LinkList, n int) {
    var p *ListNode
    //初始化随机种子
    //fmt.println(time.Now().Unix())
/*  fmt.Println(time.Now().UnixNano())
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < 10; i++ {
		fmt.Println(r.Intn(100))
	}
*/
    rand.Seed(time.Now().UnixNano())
    
    //先建立一个带头结点的单链表
    list.head.next = nil
    for(i:=0; i
//随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)
func createListTail(list *LinkList, n int){
    var p *LinkNode
    var r *LinkNode
    rand.Seed(time.New().UnixNano())
    r = list //r为指向尾部的结点
    for(i:=0; i

3.10单链表的整表删除

单链表整表删除的算法思路如下:

1.声明一结点p和q;

2.将第一个结点赋值给p;

3.循环:

        将下一个结点赋值给q;

        释放p;

        将q赋值给p。

实现代码算法如下:

//初始条件:顺序线性表L已存在,操作结果:将L重置为空表
func clearList(list *LinkList) {
    var p,q *LinkNode
    //p指向第一个结点
    p = list.head
    for (p.next != nil) {//循环到末尾
        q = p.next
        p = nil
        p = q
    }
    //头结点指针域为空
    list.head.next = nil
    return true
}

3.11 单链表结构与顺序存储结构优缺点

存储分配方式:顺序存储结构用一段连续的存储单元依次存储线性表的数据元素

单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素

时间性能:

查找:顺序存储结构O(1),单链表O(n)

插入和删除:

顺序存储结构需要平均移动表一半的元素,时间为O(n)

单链表在线出某位置的指针后,插入和删除时仅为O(1)

空间性能

顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢

单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制

3.12 静态链表

用数组来代替指针,来描述单链表。我们让数组的元素都是由两个数据域组成,data和cur。

我们把这种用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。

//线性表的静态链表存储结构
//静态链表结点
type ListNode struct {
    data string
    cursor int
}

const maxSize int = 10
//初始化链表
func initList(list *StaticLinkList){
    var i int
    for(i=0;i

3.12.1 静态链表的插入操作

静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。

为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

//若备用空间链表非空,则返回分配的结点下标,否则返回0
func malloc(list *StaticLinkList) int{
    i := list[0].cursor //当前数组第一个元素的cur存的值,就是要返回的第一个备用空间的下标
//  if i == 0 {
//        os.Exit(0)
//}
    if (list[0].cursor) {
        list[0].cursor = list[i].cursor //由于要拿出一个分量来使用了,所以我们就得把它的下一个分量用来做备用
    }
    return i
}
//在list中第i个元素之前插入新的数据元素e
func listInsert(list *StaticLinkList, i int, data string) {
    var j,k,l int
    k = maxSize - 1
    if (i < 1 || i > length(list))
        return false
    j = malloc(list) //获得空闲分量的下标
    if(j){
        list[j].data = data //将数据赋值给此分量的data
        for (l = 1;l<=i-1;i++){//找到第i个元素之前的位置
            k = list[k].cursor
        }
        list[j].cursor = list[k].cursor//把第i个元素之前的cur赋值给新元素的cur
        list[k].cursor = j //把新元素的下标赋值给第i个元素之前的cur
        return true
    }
    return false
}

3.12.2 静态链表的删除操作

//删除在L中第i个元素
func listDelete(list []Node, i int) {
    var j, k int
    if (i <1 || i > length(list))
        return false
    k = maxSize -1
    for (j = 1;j<= i-1;j++){
        k = list[k].cursor
    }
    j = list[k].cursor
    list[k].cursor = list[j].cursor
    free(list, j)
    return true
}
//将下标为k的空闲结点回收到备用链表
func free(list *StaticLinkList, k int) {
    list[k].cursor = list[0].cursor //把第一个元素cur值赋给要删除的分量cursor
    list[0].cursor = k //把要删除的分量下标赋值给第一个元素的cursor
}
//初始条件:静态链表L已存在。操作结果:返回L中数据元素个数
func listLength(list []Node) {
    j := 0
    i := list[maxSize-1].cursor
    if(i) {
        i = list[i].cursor
        j++
    }
    return j
}

3.12.3 静态链表优缺点

优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。

缺点:没有解决连续存储分配带来的表长难以确定的问题。

失去了顺序存储结构随机存取的特性。

总之,静态链表是为了给没有指针的高级语言设计的一种实现单链表能力的方法。

3.13 循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使用整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表。

其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环结束。

要将两个循环链表合并成一个表时,有了尾指针就非常简单了。

p=rearA.next //保存A表的头结点
rearA.next= rearB.next.next //将本是指向B表的第一个结点(不是头结点)赋值给rearA.next

rearB.next=p //将原A表的头结点赋值给rearB.next
p = nil //释放p

3.14 双向链表

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。

//线性表的双向链表存储结构
type dulLinkNode struct {
    data string
    prior *dulLinkNode
    next *dulLinkNode
}
type dulLinkList struct {
    head *dulLinkNode
    tail *dulLinkNode
}

既然单链表也可以有循环链表,那么双向链表当然也可以是循环表。

由于这是双向链表,那么对于链表中的某一个结点p,它的后继的前驱是谁?当然还是它自己。它的前驱的后继自然也是它自己,即:

p.next.prior=p=p.prior.next

插入操作,插入结点s

s.prior = p // 把p赋值给s的前驱
s.next = p.next //把p.next赋值给s的后继
p.next.prior = s// 把s赋值给p.next的前驱
p.next = s//把s赋值给p的后继

顺序是先搞定s的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。

删除操作,删除结点p

p.prior.next = p.next //把p.next赋值给p.prior的后继\
p.next.prior = p.prior//把p.prior赋值给p.next的前驱
p = nil//释放结点

3.15 总结回顾

线性表是零个或多个具有相同类型的数据元素的有序序列。

顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。通常我们都是用数组来实现这一结构。

由于顺序存储结构的插入和删除操作不方便,引出了链式存储结构。它具有不受固定的存储空间限制,可以比较快捷的插入和删除操作的特点。然后我们分别就链式存储结构的不同形式,如单链表、循环链表和双向链表做了讲解,还讲了不使用指针如何处理链表结构的静态链表方法。

第4章 栈与队列

栈是限定仅在表尾进行插入和删除操作的线性表。

队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。

栈 data 同线性表。元素具有相同的类型,相邻元素具有前驱和后继的关系。

操作

initStack(*S):初始化操作,建立一个空栈S。

destroryStack(*S):若栈存在,则销毁它。

clearStack(*S):将栈清空。

stackEmpty(S):若栈为空,返回true,否则返回false。

getTop(S, *e):若栈存在且非空,用e返回S的栈顶元素。

push(*S, e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。

pop(*S, *e):删除栈S中栈顶元素,并用e返回其值。

stackLength(S):返回栈S的元素个数。

4.4 栈的顺序存储结构及实现

4.4.1 栈的顺序存储结构

若存储栈的长度为stackSize,则栈顶位置top必须小于stackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判定条件定位top等于-1。

type stack struct {
    maxSize int  //规定栈最多放几个元素
    arr [5]int  //用数组模拟栈
    top int   //用于栈顶指针,目前栈顶的下标
}

4.4.2 栈的顺序存储结构--进栈操作

//插入元素data为新的栈顶元素
func push(*S stack, e string) {
    if (S.top == maxSize-1){//栈满
        return false
    }
    S.top++ //栈顶指针增加一
    S.data[S.top] = e // 将新插入元素赋值给栈顶空间
    return true
}

4.4.3 栈的顺序存储结构--出栈操作

出栈操作pop,代码如下:

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回true,否则返回false
func pop(*S stack, *e string) {
    if (S.top == -1) {
        return false
    }
    *e = S.data[S.top] //将要删除的栈顶元素赋值给e
    S.top--  //栈顶指针减一
    return true
}

4.5 两栈共享空间

数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处,另一个栈为栈的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端向中间延伸。

其实关键思路是:它们是在数组的两端,向中间靠拢。top1和top2是栈1和栈2的栈顶指针,可以想象,只要它们俩不见面,两个栈就可以一直使用。

//两栈共享空间结构
type doubeStack struct {
    maxSize int
    arr [20]int
    top1 int  //栈1栈顶指针
    top2 int  //栈2栈顶指针
}

对于两栈共享空间的push方法,除了要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stackNumber。

//插入元数e为新的栈顶元素
func(this *doubleStack) push(e int, stackNumber int) {
    //栈已满,不能再push新元素了
    if(doubleStack.top1+1 == doubleStack.top2) {
        return false
    }
    //栈1有元素进栈
    if(stackNumber==1) {
        //若栈1则先top1+1后给数组元素赋值
        top1 = top1+1
        S.data = e
    } else if(stackNumber==2) {//栈2有元素进栈
        //若栈1则先top2-1后给数组元素赋值
        top2 = top2-1
        S.data = e
    }
    return true
}

对于两栈共享空间的pop方法,参数就只是判断栈1栈2的参数stackNumber,代码如下:、

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回true,否则返回false
func (*S doubleStack) pop(e int, stackNumber int) {
    if(stackNumber==1){
        if(S.top1==-1){//说明栈1已经是空栈,溢出
            return false
        }
        e = S.data  //将栈1的栈顶元素出栈
        S.top1 = S.top1-1
    } else if(stackNumber==2) {
        if(S.top2==maxSize){//说明栈2已经是空栈,溢出
            return false
        }
        e = S.data  //将栈2的栈顶元素出栈
        S.top2 = S.top2+1
    }
    return true
}

使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。

4.6 栈的链式存储结构及实现

4.6.1 栈的链式存储结构

栈的链式存储结构,简称链栈。

对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实是就是top=nil的时候。

type stackNode struct {
    data interface{}
    next *stackNode
}
type linkStackPtr *stackNode
type linkStack struct {
    top linkStackPtr
    count int 
}

4.6.2 栈的链式存储结构--进栈操作

//插入元素e为新的栈顶元素
func (S *linkStack) push(e interface{}){
    //创建新结点s
    var s stackNode
    s.data = e
    s.next = S.top //把当前的栈顶元素赋值给新结点的直接后继
    S.top = s//将新的结点S赋值给栈顶指针
    S.count++
    return true
}

4.6.3 栈的链式存储结构--出栈操作

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回true,否则返回false
func(S *linkStack) pop(e interface{}) {
    //var p linkStackPtr
    var p *stackNode
    if (stackEmpty(S)){
        return true
    }
    e = S.top.data
    p = S.top     //将栈顶结点赋值给p
    S.top = S.top.next //使得栈顶指针下移一位,指向后一结点
    //释放结点p
    p = nil
    S.count--
    return true
}

如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。

4.7 栈的作用

4.8 栈的应用--递归

4.8.1 斐波那契数列的实现

这个数列有个十分明显的特点,那是:前面相邻两项之和,构成了后一项。

用常规的迭代实现,打印出前40位的斐波那契数列数

func main() {
    var i int
    var a[40] int
    a[0] = 0
    a[1] = 1
    fmt.Printf("%d", a[0])
    fmt.Printf("%d", a[1])
    for(i = 2;i < 40;i++){
        a[i] = a[i-1] + a[i-2]
        fmt.Printf("%d", a[i])
    }
    return 0
}

用递归来实现

//斐波那契的递归函数
func fbi(i int) {
    if(i == 0) {
        return 0
    } else if(i == 1){
        return 1
    } else {
        return fni(i-1) + fni(i-2) // 这里fbi就是函数自己,它在调用自己
    }
}
func main(){
    var i int
    for (i=0;i<40;i++) {
        fmt.Printf("%d", fbi(i))
    }
    return 0
}

4.8.2 递归定义

我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称为递归函数。

每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。

迭代使用的是循环结构,递归使用的是选择结构。

大量的递归调用会建立函数的副本,会消耗大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。

4.9 栈的应用--四则运算表达式求值

4.9.1 后缀(逆波兰)表示法定义

4.9.3 中缀表达式转后缀表达式

要想让计算机具有处理我们通常的标准(中缀)表达式的能力,最重要的就是两步:

1.将中缀表达式转化为后缀表达式(栈用来进出运算的符合)。

2.将后缀表达式进行运算得出结果(栈用来进出运算的数字)。

4.10 队列的定义

队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

4.11 队列的抽象数据类型

队列(queue)
data 
    同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
operation
    initQueue(*Q): 初始化操作,建立一个空队列Q
    destroyQueue(*Q): 若队列Q存在,则销毁它。
    clearQueue(*Q): 将队列Q清空
    queueEmpty(Q): 若队列Q为空,返回true,否则返回false。
    getHead(Q,*e): 若队列Q存在且非空,用e返回队列Q的队头元素。
    enQueue(*Q, e): 若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
    deQueue(*Q, *e): 删除队列Q中队头元素,并用e返回其值。
    queueLength(Q): 返回队列Q的元素个数

4.12 循环队列

线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。

为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。

4.12.2 循环队列定义

我们把队列的这种头尾相接的顺序存储结构称为循环队列。

队列满的条件是(rear+1)%queueSize == front

通用的计算队列长度公式为:

(rear-front+queueSize)%queueSize

循环队列的顺序存储结构代码如下:
//循环队列的顺序存储结构
type sqQueue struct {
    data []int
    front int //头指针
    rear  int // 尾指针,若队列不空,指向队列尾元素的下一个位置
}

循环队列的初始化代码如下:

//初始化一个空队列Q
func initQueue() *sqQueue{
    Q := new(sqQueue)
    Q.data = make([]int, maxSize)
    Q.front = 0
    Q.rear = 0
    return Q
}

循环队列求队列长度代码如下:

//返回Q的元素个数,也就是队列的当前长度
func queueLength(Q sqQueue){
    return (Q.rear-Q.front+maxSize)%maxSize
}

循环队列的入队列操作代码如下:

//若队列未满,则插入元素e为Q新的队尾元素
func enQueue(*Q sqQueue, e int){
    if((Q.rear+1)%maxSize == Q.front){//队列满的判断
        return false
    }
    //将元素e赋值给队尾
    Q.data[Q.rear] = e
    Q.rear = (Q.rear+1)%maxSize //rear指针向后移一位置
                                //若到最后则转到数组头部
    return true
}

循环队列的出队列操作代码如下:

//若队列不空,则删除Q中队头元素,用e返回其值
func(Q *sqQueue) deQueue() int {
    if(Q.front == Q.rear) {//队列空的判断
        return nil
    }
    e := Q.data[Q.front]
    Q.data[Q.front] = nil
    Q.front = (Q.front+1)%maxSize //front指针向后移一位置,若到最后则转到数组头部
    return e
}

4.13 队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。

链队列的结构为:

type qNode struct {
    data int
    next *qNode
}
type linkQueue struct {
    //队头、队尾指针
    front *qNode
    rear  *qNode
    count int   //所属队列
}

4.13.1 队列的链式存储结构--入队操作

//插入元素e为Q的新的队尾元素
func(lsQueue *linkQueue) enQueue(e int) *qNode {
    node := new(qNode)
    node.data = e
    node.next = nil
    //node.lsQueue = lsQueue
    lsQueue.rear.next = node //把拥有元素e新结点node赋值给原队尾结点的后继
    lsQueue.rear      = node // 把当前的node设置为队尾结点,rear指向node
    lsQueue.count++
    return node
}

4.13.2 队列的链式存储结构--出队操作

//若队列不空,删除Q的队头元素,用e返回其值,并返回true,否则返回nil
func(lsQueue *linkQueue) deQueue(e int) *qNode{
    if lsQueue.front == lsQueue.rear {
        return nil
    }
    //将欲删除的队头结点暂存给node
    node := lsQueue.front.next
    e = node.data //将欲删除的队头结点的值赋值给e
    lsQueue.front.next = node.next //将原队头结点后继node.next赋值给头结点后继
    //若队头是队尾,则删除后将rear指向头结点
    if(lsQueue.rear == node) {
        lsQueue.rear = lsQueue.front
    }
    return node
}

总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

4.14 总结回顾

4.15  结尾语

第5章 串

串是由零个或多个字符组成的有限序列,又名叫字符串。

串(string)
Data
    串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
    strAssign(T, *chars):生成一个其值等于字符串常量chars的串T
    strCopy(T,S):串S存在,由串S复制得串T
    clearString(S):串S存在,将串清空
    stringEmpty(S):若串S为空,返回true,否则返回false
    strLength(S):返回串S的元素个数,即串的长度
    strCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S

操作index的实现算法

//T为非空串。若主串S中第pos个字符之后存在与T相等的子串
//则返回第一个这样的子串在S中的位置,否则返回0
func index(S string, T string, pos int) int {
    var n,m,i int
    var sub   string
    if (pos > 0) {
        n = strLength(S)   //得到主串S的长度
        m = strLength(T)   //得到子串T的长度
        i = pos
        for (i <= n-m+1) {
            subString(sub, S, i, m) //取主串第i个位置长度与T相等子串给sub
            if (strCompare(sub,T) != 0) {//如果两串不相等
                i+1
            } else {//如果两串相等
                return i //则返回i值
            }
        }
    }
    return 0 //若无子串与T相等,返回0
}

当中用到了strLength、subString、strCompare等基本操作来实现。

5.5 串的存储结构

5.5.1 串的顺序存储结构

5.5.2 串的链式存储结构

5.6 朴素的模式匹配算法

子串的定位操作通常称为串的模式匹配

假设我们要从下面的主串S="goodgoogle"中,找到T="google"这个子串的位置。我们通常需要下面的步骤。

简单的说,就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T得长度的小循环,直到匹配成功或全部遍历完成为止。

前面我们已经用串的其他操作实现了模式匹配的算法index。现在考虑不用串的其他操作,而是只用基本的数组来实现同样的算法。注意我们假设主串S和要匹配的子串T的长度存在S[0]与T[0]中。实现代码如下:

//返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数返回值为0
//T非空,1<=pos<=strLength(S)
func index(S string, T string, pos int) int {
    i := pos //i用于主串S中当前位置下标,若pos不为1,则从pos位置开始匹配
    j := 1   //j用于子串T中当前位置下标值
    for (i <= S[0] && j <= T[0]) {//若i小于S长度且j小于T的长度时循环
        if (S[i] == T[j]) {//两字符相等则继续
            i + 1
            j + 1
        } else {//指针后退重新开始匹配
            i = i-j+2  //退回到上次匹配首位的下一位
            j = 1      //j退回到子串T的首位
        }
    }
    if (j > T[0]) {
        return i - T[0]
    } else {
        return 0
    }
}

5.7 KMP模式匹配算法

从这个算法的研究角度来理解为什么它比朴素算法要好。

我们在朴素的模式匹配算法中,主串的i值是不断地回溯来完成的。而我们的分析发现,这种回溯其实是可以不需要的--我们的KMP模式匹配算法就是为了让这没必要的回溯不发生。

既然i值不回溯,也就是不可以变小,那么要考虑的变化就是j值了。通过观察也可发现,我们屡屡提到了T串的首字符与自身后面字符的比较,发现如果有相等字符,j值的变化就会不相同。也就是说,这个j值的变化与主串其实没什么关系,关键就取决于T串的结构中是否有重复的问题。

j值的多少取决于当前字符之前的串的前后缀的相似度。

我们把T串各个位置的j值的变化定义为一个数组next,那么next的长度就是T串的长度。

我们可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1。

5.7.3 KMP模式匹配算法实现

//通过计算返回子串T的next数组
func get_next(T string, *next int) {
    i := 1
    j := 0
    next[1] = 0
    for (i < T[0]) {//此处T[0]表示串T的长度
        if(j == 0 || T[i] == T[j]) {//T[i]表示后缀的单个字符
        //T[j]表示前缀的单个字符
            i+1
            j+1
            next[i] = j
        } else {
            j = next[j] //若字符不相同,则j值回溯
        }
    }
}

 这段代码的目的就是为了计算出当前要匹配的串T的next数组。

//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。
//T非空,1<=pos<=strLength(S)
func index_KMP(S string, T string, pos int) int {
    i := pos //i用于主串S当前位置下标值,若pos不为1,则从pos位置开始匹配
    j := 1 //j用于子串T中当前位置下标值
    var next []int  //定义一next数组
    get_next(T, next) //对串T作分析,得到next数组
    for (i <= S[0] && j <= T[0]) {//若i小于S的长度且j小于T的长度时,循环继续
        if (j == 0 || S[i] == T[j]) {//两字母相等则继续,与朴素算法增加了j=0判断
            i+1
            j+1
        } else {//指针后退重新开始匹配
            j = next[j] //j退回合适的位置,i值不变
        }
    }
    if (j > T[0]) {
        return i - T[0]
    } else {
        return 0
    }
}

加粗的为相对于朴素匹配算法增加的代码,改动不算大,关键就是去掉了i值回溯的部分。对于get_next函数来说,若T的长度为m,因只涉及到简单的单循环,其时间复杂度为O(m),而由于i值的不回溯,使得index_KMP算法效率得到了提高,for循环的时间复杂度为O(n)。因此,整个算法的时间复杂度为O(n+m)。相较于朴素模式匹配算法的O((n-m+1)*m)来说,是要好一些。

这里也需要强调,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异并不明显。

5.7.4 KMP模式匹配算法改进

假设取代的数组为nextval,增加了加粗部分,代码如下:

//求模式串T的next函数修正值并存入数组nextval
func get_nextval(T string, nextval int) {
    i := 1
    j := 0
    nextval[1] = 0
    for (i 

实际匹配算法,只需要将“get_next(T, next)" 改为”get_nextval(T,next)“即可,这里不再重复。

5.7.5 nextval数组值推导

先算出next数组的值,然后再分别判断。

总结改进过的KMP算法,它是在计算出next值的同时,如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next的值。

5.8 总结回顾

第6章 树

树是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:(1)有且仅有一个特定的称为根(root)的结点;(2)当n>1时,其余结点可分m(m>0)个互不相交的有限集T1、T2、......、Tm,其中每一个集合本身又是一颗树,并且称为根的子树。

6.2 树的定义

一对多的数据结构--”树“

对于树的定义还需要强调两点:

1.n>0时根节点是唯一的,不可能存在多个根节点。

2.m>0时,子树的个数没有限制,但它们一定是互不相交的。

6.2.1 结点分类

树的度是树内各结点的度的最大值。图例这课树结点的度的最大值是结点D的度,为3,所以树的度也为3。

6.2.2 结点间关系

6.2.3 树的其他相关该概念

树中结点的最大层次称为树的深度或高度,当前树的深度为4。

对比线性表与树的结构

线性结构 第一个数据元素:无前驱;最后一个数据元素:无后继;中间元素:一个前驱一个后继。

树结构 根结点:无双亲,唯一;叶子结点:无孩子,可以多个;中间结点:一个双亲多个孩子

6.3 树的抽象数据类型

树
data 树是由一个根结点和若干颗子树构成。树中结点具有相同数据类型和层次关系。
operation
initTree(*T): 构造空树T。
destroyTree(*T): 销毁树T。
createTree(*T, definition): 按definition中给出树的定义来构造树。
clearTree(*T): 若树T存在,则将树T清为空树。
treeEmpty(T): 若T为空树,返回true,否则返回false。
treeDepth(T): 返回T的深度。
root(T): 返回T的根结点。
value(T,cur_e): cur_e是树T中一个结点,返回此结点的值。
assign(T,cur_e,value):给树T的结点cur_e赋值为value。
parent(T,cur_e): 若cur_e是树T的非根结点,则返回它的双亲,否则返回空。
leftChild(T,cur_e):若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空。
right_Sibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则返回空。
insertChild(*T, *p,i, c):其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交,操作结果为插入c为树T中p指结点的第i颗子树。
deleteChild(*T, *p, i): 其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i颗子树。

充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。我们这里要介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法。

6.4.1 双亲表示法

以下是我们的双亲表示法的结点结构定义代码。

//树的双亲表示法结点结构定义
var max_tree_size int = 100
var TElemType int // 树结点的数据类型,目前暂定为整型
type PTNode struct {//结点结构
    data TElemType //结点数据
    parent int     //双亲位置
}
type PTree struct { //树结构
    nodes[max_tree_size] PTNode //结点数组
    r,n int //根的位置和结点数
}

6.4.2 孩子表示法

以下是我们的孩子表示法的结构定义代码

// 树的孩子表示法结构定义
var MAX_TREE_SIZE 100
// 孩子结点
type CTNode struct {
    child int
    Next  *CTNode
}
// 表头结构
type CTBox struct {
    data TElemType
    firstchild ChildPtr
}
// 树结构
type CTree struct {
    nodes[MAX_TREE_SIZE] CTBox //结点数组
    r, n int   //根的位置和结点数
}

6.4.3 孩子兄弟表示法

结构定义代码如下。

// 树的孩子兄弟表示法结构定义
type CSNode struct {
    data TElemType 
    firstchild, rightsib *CSNode
}

6.5 二叉树的定义

6.5.1 二叉树特点

6.5.2 特殊二叉树

1.斜树

2.满二叉树

3.完全二叉树

6.6 二叉树的性质

6.6.1 二叉树性质1

在二叉树的第i层上至多有2^(i-1)个结点(i>=1)

6.6.2 二叉树性质2

深度为k的二叉树至多有2^k - 1个结点(k >= 1)

6.6.3 二叉树性质3

对任何一颗二叉树T,如果其终点结点数为n0,度为2的结点数为n2,则n0=n2+1

6.6.4 二叉树性质4

具有n个结点的完全二叉树的深度为(log2 n) +1 ((x)表示不大于x的最大整数)

6.6.5 二叉树性质5

如果对一颗有n个结点的完全二叉树(其深度为(log2 n) +1)的结点按层序编号(从第1层到第(log2 n) +1层,每层从左到右),对任一结点i(1=< i =

有:1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点(i/2)

2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i

3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。

6.7 二叉树的存储结构

6.7.1 二叉树顺序存储结构

6.7.2 二叉链表

以下是我们的二叉链表的结点结构定义代码

// 二叉树的二叉链表的结点结构定义
type BiTNode struct {// 结点结构
    data TElemType  // 结点数据
    lchild,rchild *BiTNode // 左右孩子指针
}

6.8 遍历二叉树

6.8.1 二叉树遍历原理

二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

这里有两个关键词:访问和次序。

6.8.1 二叉树遍历方法

1.前序遍历

规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

2.中序遍历

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。

3.后序遍历

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。

4.层序遍历

规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

6.8.3 前序遍历算法

// 二叉树的前序遍历递归算法
func PreOrderTraverse (T Bitree) {
    if T == nil 
        return
    fmt.Printf("%d",T.data) //显示节点数据,可以更改为其他对节点操作
    PreOrderTraverse(T.lchild) //再先遍历左子树
    PreOrderTraverse(T.rchild) // 最后先序遍历右子树
}

6.8.4 中序遍历算法

// 二叉树的中序遍历递归算法
func InOrderTraverse (T Bitree) {
    if T == nil 
        return
    InOrderTraverse(T.lchild) //中序遍历左子树
    fmt.Printf("%d",T.data) //显示结点数据他,可以更改为其他对结点操作
    InOrderTraverse(T.rchild) // 最后中序遍历右子树
}

6.8.5 后序遍历算法

// 二叉树的后序遍历递归算法
func PostOrderTraverse(T Bitree) {
    if T == nil
        return
    PostOrderTraverse(T.lchild) // 先后序遍历左子树
    PostOrderTraverse(T.rchild) // 再后序遍历右子树
    fmt.Printf("%d",T.data) //显示结点数据,可以更改为其他对结点操作
}

二叉树遍历的性质

已知前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。

已知后序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。

6.9 二叉树的建立

由标明空子树的先跟遍历序列建立一颗二叉树的操作算法

func HaveEmptyCreate(arr []int) *TreeNode {
    i = i + 1
    if i >= len(arr) {
        return nil
    }
    var t TreeNode
    if arr[i] != 0 {
        t = TreeNode{nil,arr[i],nil}
        t.Left = HaveEmptyCreate(arr)
        t.Right = HaveEmptyCreate(arr)
    } else {
        return nil
    }
    return &t 

}

由完全二叉树的顺序存储结构建立其二叉链式存储结构

func TreeCreate(i int,arr []int) *TreeNode {
    t := &TreeNode{nil,arr[i],nil}
    if i

6.10 线索二叉树

6.10.1 线索二叉树原理

6.10.2 线索二叉树结构实现

6.11 树、森林与二叉树的转换

6.11.1 树转换为二叉树

6.11.2 森林转换为二叉树

6.11.3 二叉树转换为树

6.11.4 二叉树转换为森林

6.11.5 树与森林的遍历

6.12 赫夫曼树及其应用

6.12.1 赫夫曼树

6.12.2 赫夫曼树定义与原理

带权路径长度WPL最小的二叉树称做赫夫曼树,也称为最优二叉树

构造赫夫曼树的赫夫曼算法描述:

1.根据给定的n个权值{w1,w2,...,wn}构成n棵二叉树的集合F={T1,T2,...,Tn},

2.在F中选取两颗根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树根结点的权值之和。

3.在F中删除这两颗树,同时将新得到的二叉树加入F中。

4.重复2和3步骤,直到F只含一颗树为止。这颗树便是赫夫曼树。

6.12.3 赫夫曼编码

6.13 总结回顾

第7章 图

7.2 图的定义

图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

7.2.1 各种图定义

7.2.2图的顶点与边间关系

7.2.3 连通图相关术语

7.2.4 图的定义与术语总结

7.3 图的抽象数据类型

7.4 图的存储结构

7.4.1 邻接矩阵

// 图的邻接矩阵存储的结构

有了这个结构定义,我们构造一个图,其实就是给顶点表和边表输入数据的过程。

// 建立无向网图的邻接矩阵表示

7.4.2 邻接表

7.4.3 十字链表

7.4.4 邻接多重表

7.4.5 边集数组

7.5 图的遍历

图的遍历和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。

7.5.1 深度优先遍历

深度优先遍历其实是一个递归的过程,还有就像是一棵树的前序遍历。它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。

// 邻接矩阵的深度优先递归算法

// 邻接矩阵的深度遍历操作

// 邻接表的深度优先递归算法

// 邻接表的深度遍历操作

7.5.2 广度优先遍历

图的广度优先遍历类似于树的层次遍历

// 邻接矩阵的广度优先递归算法

// 邻接矩阵的广度遍历操作

// 邻接表的广度优先递归算法

// 邻接表的广度遍历操作

7.6 最小生成树

找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。

7.6.1 普里姆(Prim)算法

// Prim算法生成最小生成树

7.6.2 克鲁斯卡尔( Kruskal )算法

// 对边集数组Edge结构的定义
type Edge struct {
    begin int
    end   int
    weight int
} 

// Kruskal算法生产最小生成树

7.7 最短路径

对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。

7.7.1 迪杰斯特拉(Dijkstra)算法

基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终得到你要的结果。

7.7.2 弗洛伊德(Floyd)算法

// Floyd算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w]

求最短路径的显示代码可以这样写:

//获得第一个路径顶点线标

// 打印源点

// 如果路径顶点下表不是终点

// 打印路径顶点

// 获得下一个路径顶点下标

//打印终点

7.8 拓扑排序

7.8.1 拓扑排序介绍

7.8.2 拓扑排序算法

在拓扑排序算法中,涉及的结构代码如下:

// 边表结点

// 邻接点域,存储该顶点对应的下标

// 用于存储权值,对于非图网可以不需要

// 链域,指向下一个邻接点

// 顶点表结点

// 顶点入度

// 顶点域,存储顶点信息

// 边表头指针

// 图中当前顶点数和边数

在算法中,我还需要辅助的数据结构一栈,用来存储处理过程中入度为0的顶点,目的是为了避免每个查找时都要去遍历顶点表找有没有入度为0的顶点。

// 拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR

// 用于栈指针下标

// 用于统计输出顶点的个数

// 建栈存储入度为0的顶点

// 将入度为0的顶点入栈

// 出栈

// 打印此顶点

// 统计输出顶点数

// 对此顶点弧表遍历

// 将k号顶点邻接点的入度减1

// 若为0则入栈,以便于下次循环输出

// 如果count小于顶点数,说明存在环

7.9 关键路径

7.9.1 关键路径算法原理

7.9.2 关键路径算法

7.10 总结回顾

图的应用是我们这 章浓墨重彩的 部分 共谈了 种应用 最小生成树、最 短路径和有向无环图的应用. 最小生成树,我们讲了两种算法:普里姆 Prim 算法和克鲁斯卡尔( ka1 算法。普里姆算法像是走一步看一步的思维方式,逐步生成最小生成树。而克鲁斯卡 尔算法则更有全局意识,直接从圈中最短权值的边入手,找寻最后的答案。 最短路径的现实应用非常多,我们也介绍了两种算法。迪杰斯特拉 Dijkstra 法更强调单源顶点查找路径的方式,比较符合我们正常的思路,容易理解原理,但算 法代码相对复杂。而弗洛伊德 Fbyd 算法则完全抛开 单点的局限思维方式,巧妙 地应用矩阵的变换,用最清爽的代码实现 多顶点间最短 程求解的方案, 理理解 有难度,但算法编写很简洁。 有向无环圈时常应用于工程规划中,对于整个工程或系统来说,我们 方面关心 的是工程能否顺利进行的问题,通过拓 排序的方式,我们可以有效地分析出 个有 向图是否存在环,如果不存在,那色的拓扑序列是什么?另 方面关心的是整个工程 完成所必须的最短时间问题,利用求关键路径的算法,可以得到最短完成工程的工期 以及关键的活动有哪些。

第8章 查找

8.3.1 顺序表查找算法

// 顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字
func Sequential_Search(a []int, n int, key int) int {
    var i int
    for i = 1; i <= n; i++ {
        if a[i] == key     
            return i
    }
    return 0
}

8.3.2 顺序表查找优化

// 有哨兵顺序查找
func Sequential_Search2(a []int, n int, key int) int {
    var i int 
    a[0] = key // 设置a[0]为关键字值,我们称之为“哨兵”
    i = n      // 循环从数组尾部开始
    while(a[i] != key) {
        i --
    }

    return i // 返回0则说明查找失败
}

8.4 有序表查找

8.4.1 折半查找

二分查找

前提是线性表中的记录 必须是关键码有序(通常从小到大有序) ,线性表必须采用顺序存储。折半查找的基 本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相 等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找; 若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

// 折半查找
func Binary_Search(a []int, n int, key int) {
    var low,high,mid int
    low = 1  // 定义最低下标为记录首位
    high = n // 定义最高下标为记录首位
    while(low <= high) {
        mid = (low +high)/2  // 折半
        if (keya[mid]) // 若查找值比中值大
            low = mid + 1 // 最低下标调整到中位下标大一位
        else
            return mid   // 若相等则说明mid即为查找到的位置
    }
    return 0
}

因此最终我们折半算法的时间复杂度为 0(log n) ,它显然远远好于顺序查找的 0(n) 时间复杂度了

8.4.2 插值查找

mid=low+ (high-low) * (key-a [low]) / (a[high]-a[low] ) ; /*插值*/

插值查找(Interpolation Search)是 根据要查找的关键字 key 与查找表中最大最小记录的关键字比较后的查找方法,其核心 就在于插值的计算公式(key-a [low]) / (a[high]-a[low] )

8.4.3 斐波那契查找

// 斐波那契查找
func Fibonacci_Search(a []int, n int, key int) {
    var low,high,mid,i,k int
    low = 1 // 定义最低下标为记录首位
    high = n // 定义最高下标为记录末位
    k = 0
    while(n > F[k] -1) //计算n位于斐波那契数列的位置
        k++
    for i = n; i a[mid]) {// 若查找记录大于当前分隔记录
            low = mid +1 //最低下标调整到分隔下标mid+1处
            k = k-2      // 斐波那契数列下标减两位
        } 
        else {
            if (mid<=n)
                return mid // 若相等则说明mid即为查找到的位置
            else
                return n   // 若mid>n说明是补全数值,返回n
        }
    }
    return 0
}

斐波那契查找算法的核心在于:

1) 当key=a[mid] 时,查找就成功;

2)当key

3)当key>a[mid] 时,新范围是第m+1个到第high个,此时范围个数为F[k-2]-1个

《大话数据结构》golang代码练习_第1张图片

 8.5 线性索引查找

索引就是把一个关键字与它对应的记录相关联的过程

稠密索引、分块索引和倒排索引。

稠密索引是指 线性索引 ,将数据集中的每个记录对应一个索引项,

分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:

块内无序

块内无序

其中记录号表存储具有相同次关键字的所有记录的记录号 (可以是指向记录的指 针或者是该记录的主关键字) 这样的索引方法就是倒排索引 (invened index) 。

二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者 是具有下列性质的二叉树。

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

若它的右子树不空 ,则右子树上所有结点的值均大于它的根结点的值;

它的左、右子树也分别为二叉排序树。

构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。

//二叉树的二叉链表结点结构定义

二叉排序树的查找是如何实现的

// 递归查找二叉排序树T中是否存在key

// 指针f指向T的双亲,其初始调用值为NULL

// 若查找成功,则指针p指向该数据元素2结点,并返回TRUE

// 否则指针p指向查找路径上访问的最后一个结点并返回FALSE

8.6.2 二叉排序树插入操作

// 当二叉排序树T中不存在关键字等于key的数据元素时

//插入key并返回TRUE,否则返回FALSE

创建一颗二叉排序树

func main() {
    var i int 
    var a [10]{62,88,58,47,35,73,51,99,37,93}
    type T BiTree {}
    for i=0; i<10;i++
        InsertBST(&T,a[i])
}

8.6.3 二叉排序树删除操作

比较好的办法就是,找到需要删除的结点p的直接前驱(或直接后继) s, 用s来替换结点p,然后再删除结点s

根据我们对删除结点三种情况的分析:

• 叶子结点;

• 仅有左或右子树的结点

• 左右子树都有的结点,我们来看代码,下面这个算法是递归方式对二叉排序树T查找 key. 查找到时删除

//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,并返回TRUE,否则返回FALSE

//从二叉排序树中删除结点p,并重接它的左或右子树

8.7 平衡二叉树(AVL树)

平衡二叉树,是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1.

8.7.1 平衡二叉树实现原理

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点 时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

《大话数据结构》golang代码练习_第2张图片

 8.7.2 平衡二叉树实现算法

首先是需要改进二叉排序树的结点结构,增加一个bf,用来存储平衡因子。

对于右旋操作,代码

// 对以p为根的二叉排序树作右旋处理

// 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点

// L指向P的左子树根结点

// L的右子树挂接为P的左子树

// P指向新的根结点

左旋操作代码

//对以P为根的二叉排序树作左旋处理

//处理之后P指向新的树根结点,即旋转处理之前的右子树的根结点0

//R指向P的右子树根结点

//R的左子树挂接P的右子树

//P指向新的根结点

8.8 多路查找树(B树)

多路查找树 muitl-way search tree ,其每一个结点的孩子数可以多于两 个,且每一个结点处可以存储多个元素 。由于它是查找树,所有元素之问存在某种特定的排序关系。

特殊形式 2-3树, 2-3-4树、B树和B+树。

B树是一种平衡的多路查找树,2-3树和 2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶 (order) ,因此, 2-3 树是3阶B树, 2-3-4树是4阶B树。

8.9 散列表查找(哈希表)概述

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字 key 对应一个存储位置f (key) 。

8.10 散列函数的构造方法

1.计算简单 2.散列地址分布均匀

8.10.1 直接定址法

8.10.2 数字分析法

8.10.3 平方取中法

8.10.4 折叠法

8.10.5 除留余数法

8.10.6 随机数法

现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考: 1.计算散列地址所需的时间。 2.关键字的长度。 3.散列表的大小。 4.关键字的分布情况。 5.记录查找的频率。综合这些因素,才能决策选择哪种散列函数更合适。

8.11.1 开放定址法

线性探测法

随机探测法

8.11.2 再散列函数法

8.11.3 链地址法

8.11.4 公共溢出区法

8.12.2 散列表查找性能分析

1.散列函数是否均匀

2.处理冲突的方法

3.散列表的装填因子

首先我们要弄清楚查找表、记录、关键字、主关键字、静态查找表、动态查找表等这些概念。

第9章 排序

多个关键字的排序最终都可以转化为单个关键字的排序

9.2.1 排序的稳定性

假设ki=kj(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中ri领先于rj(即i

9.2.2 内排序与外排序

内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

1.时间性能

2.辅助空间

3.算法的复杂性

我们把内排序分为:插入排序、交换排序、选择排序和归并排序。

按照算法的复杂度分为两大类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。

9.3 冒泡排序

9.3.1最简单排序实现

冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

func BubbleSort0(L SqList) {
    var i, j int
    for i=1;iL[j]) {
                // 交换L[i]>L[j]
                swap(L,i,j)
            }
        }
    }
}

9.3.2 冒泡排序算法

func BubbleSort0(L SqList) {
    var i, j int
    for i=1;ii;j-- {
            // 若前者大于后者
            if (L[j]>L[j+1]) {
                // 交换
                swap(L,j,j+1)
            }
        }
    }
}

9.3.3冒泡排序优化

// 对顺序表L作改进冒泡算法
func BubbleSort2(L SqList) {
    var i,j int
    // flag用来作为标记
    flag := true
    // 若flag为true说明有过数据交换,否则停止循环
    for i=1;i=i;j-- {
            if (L[j] >L[j+1]) {
                swap(L,j,j+1)
                // 如果有数据交换,则flag为true
                flag = true
            }
        }
    }
}

9.3.4 冒泡排序复杂度分析

9.4 简单选择排序

选择排序的基本思想是每一趟在n-i+1(i=1,2,...,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录。

简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。

func SelectSort(L SqList) {
    var i,j,min int
    for i=1;iL[j]) {
                // 将此关键字的下标赋值给min
                min = j
            }
            // 若min不等于i,说明找到最小值,交换
            if i != min {
                // 交换L[i]与L[min]
                swap(L,i,min)
            }
        }
    }
}

9.4.2 简单选择排序复杂度分析

9.5 直接插入排序

直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

func InsertSort(L SqList) {
    var i,j int

    for i =2;iL[0];j++ {
                // 记录后移
                L[j+1] = L[j]
                // 插入到正确位置
                L[j+1] = L[0]
            }
        }
    }
}

9.6 希尔排序

// 对顺序表L作希尔排序
func ShellSort(L List) {
    var i,j int
    increment := len(L)
    while (increment > 1) {
        // 增量序列
        increment = increment/3 +1
        for i = increment+1;i0&& L[0]

9.7堆排序

堆排序(HeapSort),就是对简单选择排序进行的一种改进,这种改进的效果是非常明显的。

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。

// 对顺序表L进行堆排序
func HeapSort(L List) {
    var i int
    // 把L中的r构建成一个大顶堆
    for (i = len(L)/2;i>0;i--) {
        HeapAdjust(L, i,len(L))
    }
    for (i=len(L); i>1;i--) {
        // 将堆顶记录和当前未经排序子序列的最后一个记录交换
        swap(L,l,i)
        // 将L[l..i-1]重新调整为大顶堆
        HeapAdjust(L, l, i-1)
    }
}

HeapAdjust(堆调整)函数是如何实现

// 已知L[s..m]中记录的关键字除L[s]之外均满足堆的定义
// 本函数调整L[s]的关键字,使L[s..m]成为一个大顶堆
func HeapAdjust(L List, s int, m int) {
    var temp,j int
    temp = L[s]
    // 沿关键字较大的孩子结点向下筛选
    for (j=2*s;j<=m;j*=2) {
        if (j= L[j]) {
            // rc应插入在位置s上
            break
        }
        L[s] = L[j]
        s = j
    }
    // 插入
    L[s] = temp
}

9.8归并排序

我们将本是无序的数组序列{16,7,13,10,9,15,3,2,5,8,12,1,11,4,6,14},通过两两合并排序后再合并,最终获得了一个有序的数组。

// 对顺序表作归并排序
func MergeSort(L List) {
    MSort(L,L,l,len(L))
}

MSort的实现。

// 将SR[s..t] 归并排序为TR1[s..t]
func MSort(SR[] int, TR1[] int, s int, t int) {
    var m int
    var TR2 []int
    if (s==t) {
        TR1[s] = SR[s]
    } else {
        // 将SR[s..t]平分为SR[s..m]和SR[m+1..t]
        m = (s+t)/2
        // 递归将SR[s..m]归并为有序的TR2[s..m]
        MSort(SR, TR2, s, m)
        // 递归将SR[m+1..t]归并为有序TR2[m+1..t]
        MSort(SR, TR2, m+1, t)
        // 将TR2[s..m]和TR2[m+1..t]
        // 归并到TR1[s..t]
        Merge(TR2,TR1,s,m,t)
    }
}

Merge函数的代码是如何实现的。

// 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]
func Merge(SR[] int, TR[] int, i int, m int, n int) {
     var j, k,l

     // 将SR中记录由小到大归并入TR
    for (j=m+1,k=i;i<=m&&j<=n;k++){
        if (SR[i]

9.8.3 非递归实现归并排序

9.9 快速排序

希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序其实就是我们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。

9.9.1 快速排序算法

快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

// 对顺序表L作快速排序
func QuickSort(L List) {
    QSort(L,l,Len(L))
}

QSort的实现

// 对顺序表L中的子序列L[low..high]作快速排序
func Qsort(L List, low int, high int) {
    var pivot int
    if (low < high) {
        // 将L[low..high]一分为二,算出枢轴值pivot
        pivot = Partition(L, low,high)
        // 对低子表递归排序
        QSort(L, low, pivot-1)
        // 对高子表递归排序
        QSort(L,pivot+1,high)
    }
}

快速排序最关键的Partition函数实现。

// 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置
// 此时在它之前(后)的记录均不大(小)与它
func Partition(L List, low int, high int) {
    var pivotkey int
    // 用子表的第一个记录作枢轴记录
    pivotkey = L[low]
    // 从表的两端交替向中间扫描
    while(low=pivotkey) {
            high--
            
        }
        // 将比枢轴记录小的记录交换到低端
        swap(L,low,high)
        while(low

Partition函数,其实就是将选取的pivotkey不断交换,将比它小的换到它的左边,比它大的换到它的右边,它也在交换中不断更改自己的位置,直到完全满足这个要求为止。

由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

9.9.3 快速排序优化

1.优化选取枢轴

2.优化不必要的交换

3.优化小数组时的排序方案

4.优化递归操作

5.了不起的排序算法

《大话数据结构》golang代码练习_第3张图片

 

待更新

你可能感兴趣的:(面试准备,数据结构)