Golang切片传引用的注释事项,有向图数据结构的生成,深度优先与广度优先遍历

12 篇文章 0 订阅

1、Golang切片

Go数组与C++数组一样,理论上,在定义时就要使用常数字面值定义数组大小,然后Go将对数组初始化为元素类型的零值,C++则不会进行初始化,只是返回

Go:

var arr [3]int   //自动初始化为{0,0,0}

arr = [3]int{1, 2, 3}   //

C++:

静态数组:规范的用法是在数组定义时使用常数字面值定义数组大小,从而使静态数组在编译期间就能确定要分配的栈内存大小。但有的编译器,比如darwin的clang++,也是可以用变量指定数组大小的。动态数组顾名思义自然是在定义时使用变量定义动态数组大小。

HaypinsMBP:cpptest haypin$ clang++ -v
Apple clang version 12.0.0 (clang-1200.0.32.28)
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
HaypinsMBP:cpptest haypin$
/*
内置基本类型 :初始化为对应类型的零值
类类型初始化 :将调用构造函数,不显式初始化时调用自定义的无参构造函数
或默认构造函数,显式初始化时调用对应签名的自定义构造函数
*/
int len(5);
int arr[len];               //不会初始化
int *parr=new int[len];     //视平台决定是否初始化,darwin会进行初始化
int *parr2=new int[len]();  //显式初始化为类型的零值
for(int i=0;i<len;i++){
    cout<<arr[i]<<"\t"<<parr[i]<<"\t"<<parr2[i]<<"\n";
}
输出:
-393861808      0       0
32766   0       0
121262296       0       0
1       0       0
5       0       0

Go切片其实是对Go数组的一层封装,数组是定长的,而切片是对底层数组的引用,属性len是底层数组当前的元素数目,属性cap是底层数组当前的元素容量,当通过append(slice,element)向切片添加元素超过底层数组的cap容量后,切片将重新申请一个新底层数组空间,大小比旧底层数组要大,一般是两倍,并将旧底层数组的所有元素一一拷贝到新底层数组,然后继续append要添加的元素到新底层数组,当然,len和cap属性都是相对于新底层数组的了。这行为类似于C++的vector,默认都是扩容旧vector维数的两倍。

var mysli []string //定义但没有分配内存
fmt.Printf("%p %d %d\n", mysli, len(mysli), cap(mysli))	//0x0 0 0
mysli = make([]string, 2, 2) //分配了内存并默认初始化为{0,0} 
fmt.Printf("%p %d %d\n", mysli, len(mysli), cap(mysli)) //0xc00000c0a0 2 2
mysli = append(mysli, "heiehi") //添加元素,扩容2倍,旧底层数组拷贝到新底层数组,地址发生改变
fmt.Printf("%p %d %d\n", mysli, len(mysli), cap(mysli)) //0xc000104040 3 4
mysli = append(mysli, "haha") 
fmt.Printf("%p %d %d\n", mysli, len(mysli), cap(mysli)) //0xc000104040 4 4

mysli = append(mysli, "wawa") 
fmt.Printf("%p %d %d\n", mysli, len(mysli), cap(mysli)) //0xc000108000 5 8

2、Golang切片传引用的注意事项

Go的实参向形参总是传值,值类型比如数组、字符串、数字就是值拷贝,引用类型比如切片、map是传引用,因为传递切片变量、map变量本身就是传递引用,本质上是拷贝指针的值,只有索引切片的元素、map的value才是取值。

形参切片只当通过slice[i]=xxx索引切片底层数组的元素进行赋值,才会影响实参切片。填充新元素(仍在cap内)、扩容(超出cap)以及扩容后的操作均不会影响实参切片

    sli := make([]string, 1, 2)
	fmt.Printf("实参:%v %p len:%d cap:%d\n", sli, sli, len(sli), cap(sli)) //实参:[] 0xc00000c140 len:1 cap:2
	changeSlice(sli)
	fmt.Printf("实参:%v %p len:%d cap:%d\n", sli, sli, len(sli), cap(sli)) //实参:[heihei] 0xc00000c140 len:1 cap:2
}

func changeSlice(sli []string) {
	sli[0] = "heihei"  //3、只有直接通过sli[i]="xx"索引切片底层数组的元素进行赋值才会反映到实参切片
	fmt.Printf("形参:%v %p len:%d cap:%d\n", sli, sli, len(sli), cap(sli)) //形参:[heihei] 0xc00000c140 len:1 cap:2
	sli = append(sli, "haha") //1、填充的新元素(仍在cap内)不会反映到实参切片
	fmt.Printf("形参:%v %p len:%d cap:%d\n", sli, sli, len(sli), cap(sli)) //形参:[heihei haha] 0xc00000c140 len:2 cap:2
	sli = append(sli, "enen") //2、扩容(超出cap)以及扩容后的操作不会反映到实参切片
	fmt.Printf("形参:%v %p len:%d cap:%d\n", sli, sli, len(sli), cap(sli)) //形参:[heihei haha enen] 0xc000104080 len:3 cap:4
}

所以Go切片的传引用不同于C++ vector的传引用,C++实参vector传引用给形参vector,那么形参vector进行扩容将直接影响实参:

    vector<string> varr;      //0x7ffee24654c0  0       0
    cout<<&varr<<"\t"<<varr.size()<<"\t"<<varr.capacity()<<"\n";
    changeVector(&varr);       //0x7ffee24654c0  65539   131072
    cout<<&varr<<"\t"<<varr.size()<<"\t"<<varr.capacity()<<"\n";
}
void changeVector(vector<string>* pvarr){
    pvarr->push_back("heihei"); //0x7ffee24654c0  1       1
    cout<<pvarr<<"\t"<<pvarr->size()<<"\t"<<pvarr->capacity()<<"\n";
    pvarr->push_back("haha");   //0x7ffee24654c0  2       2
    cout<<pvarr<<"\t"<<pvarr->size()<<"\t"<<pvarr->capacity()<<"\n";
    pvarr->push_back("papa");   //0x7ffee24654c0  3       4
    cout<<pvarr<<"\t"<<pvarr->size()<<"\t"<<pvarr->capacity()<<"\n";
    for(int i=0;i<1<<16;i++){
        pvarr->push_back("papa"); 
    }                         //0x7ffee24654c0  65539   131072()
    cout<<pvarr<<"\t"<<pvarr->size()<<"\t"<<pvarr->capacity()<<"\n";
    /*C++的vector扩容可以理解为"原地扩容",vector的地址扩容后不发生改变(鲁豫我不信,但现象是如此),
      所以当实参vector传引用给形参,
      形参的对元素的赋值、cap内的push_back、超出cap的扩容、扩容后的操作都会反映到实参,不会发生Go切片
      那样"形参切片扩容后就指向新开辟的底层数组,从而与实参切片失去关联"的现象,
      是不是让Go切片"无地自容"?
      Go切片传引用给形参,若想让形参切片的所有操作(cap内的append、超出cap扩容)都能反映到实参切片,就必须
      返回形参切片给实参切片,也就是像sli=append(sli,"heiehi")这样,将append(sli,"heihei")可能是扩容
      后的切片回传给实参切片,实现同步。当然,如果发生了扩容,那么实参切片就指向扩容后的新底层数组而与旧底层
      数组解耦了。利用Go切片的这一特性可以实现实参切片与形参切片解耦,但谁会这么干呢?
    */
}

C++的vector扩容可以理解为"原地扩容",vector的地址扩容后不发生改变(鲁豫我不信,但现象是如此),所以当实参vector传引用给形参,形参的对元素的赋值(arr[i]=newValue)、cap内的push_back、超出cap的扩容、扩容后的操作都会反映到实参,不会发生Go切片那样"形参切片扩容后就指向新开辟的底层数组,从而与实参切片失去关联"的现象,是不是让Go切片"无地自容"?

Go切片传引用给形参,若想让形参切片的所有操作(cap内的append、超出cap扩容)都能反映到实参切片,就必须返回形参切片给实参切片,也就是像sli=append(sli,"heiehi")这样,将append(sli,"heihei")可能是扩容后的切片回传给实参切片,实现同步。当然,如果发生了扩容,那么实参切片就指向扩容后的新底层数组而与旧底层数组解耦了。利用Go切片的这一特性可以实现实参切片与形参切片解耦,但谁会这么干呢?

还有一点:Go允许返回局部变量的引用,go build编译时侦测到如果返回了局部变量的引用,则运行时会将局部变量存储在堆中,而不是随着函数执行结束返回后完全销毁函数帧,可能是采用的引用计数的GC垃圾回收机制,返回局部变量的引用后,如果主调方准备了变量"接收"返回的局部变量引用,则由于局部变量的引用计数不是0,故不会销毁局部变量,从而使引用有效。

3、有向图的生成

素材取自《The Go Programming Language》

给定一些计算机课程, 每个课程都有前置课程, 只有完成了前置课程才可以开始当前课程的学习; 我们的目标是选择出一组课程, 这组课程必须确保按顺序学习时, 能全部被完成。 每个课程的前置课程如下:
gopl.io/ch5/toposort
// prereqs记录了每个课程的前置课程
var prereqs = map[string][]string{
"algorithms": {"data structures"},
"calculus": {"linear algebra"},
"compilers": {
"data structures",
"formal languages",
"computer organization",
},
"data structures": {"discrete math"},
"databases": {"data structures"},
"discrete math": {"intro to programming"},
"formal languages": {"discrete math"},
"networks": {"operating systems"},
"operating systems": {"data structures", "computer organization"},
"programming languages": {"data structures", "computer organization"},
}
这类问题被称作拓扑排序。 从概念上说, 前置条件可以构成有向图。 图中的顶点表示课程,
边表示课程间的依赖关系。 显然, 图中应该无环, 这也就是说从某点出发的边, 最终不会回
到该点。 

首先定义有向图的数据结构,有向图与树的区别是,有向图的父节点不止有一个,因而有向图可能具有跨层次的关系,而树的每个节点只有一个父节点:

//Node 有向图节点
type Node struct {
	name        string
	childNodes  []*Node //学习完当前课程后才可以学习的"进阶"课程
	parentNodes []*Node //学习当前课程需要首先学习的"基础"课程
}

生成有向图的Go代码:

func main(){
    var prereqs = map[string][]string{
		"algorithms": {"data structures"},
		"calculus":   {"linear algebra"},
		"compilers": {
			"data structures",
			"formal languages",
			"computer organization",
		},
		"data structures":       {"discrete math"},
		"databases":             {"data structures"},
		"discrete math":         {"intro to programming"},
		"formal languages":      {"discrete math"},
		"networks":              {"operating systems"},
		"operating systems":     {"data structures", "computer organization"},
		"programming languages": {"data structures", "computer organization"},
	}

	var dict map[string]*Node //映射节点值-节点
	dict = make(map[string]*Node)
	for strchild, nodes := range prereqs {
		var nodechild *Node //进阶课程,子节点
		if dict[strchild] != nil {
			nodechild = dict[strchild]
		} else {
			nodechild = &Node{name: strchild}
		}
		dict[strchild] = nodechild
		for _, strparent := range nodes { //基础课程,父节点
			var nodeparent *Node
			if dict[strparent] != nil {
				nodeparent = dict[strparent]
			} else {
				nodeparent = &Node{name: strparent}
			}
			dict[strparent] = nodeparent
            //形成方向
			nodechild.parentNodes = append(nodechild.parentNodes, nodeparent)
			nodeparent.childNodes = append(nodeparent.childNodes, nodechild)
		}
	}
}

4、深度优先打印有向图:

这里只是一个示意,因为有向图的节点具有多个父节点,因此对不同的父节点可能会有同一个子节点,要是在CAD中打印就很简单了,给每个节点一个坐标,那么节点间关系只用画出父节点——>子节点的连线即可。命令行要这么做可真是太复杂了。这里为每个父节点都打印一个子节点,就看个热闹:

func main(){
    ......
    //打印有向图树
	var root []*Node                            //根节点们,没有父节点的节点
	var rootroot *Node = &Node{name: "ancient"} //所有课程的祖先课程,指针的零值是nil
	for _, node := range dict {
		if node.parentNodes == nil {
			root = append(root, node)
			// printTree(node)
			rootroot.childNodes = append(rootroot.childNodes, node)
		}
	}
	printTree(rootroot)
}
var numspace int = 0

func printTree(node *Node) {
	fmt.Printf("%*s%s\n", numspace, "", node.name)
	numspace += 4    //准备打印子节点,准备缩进
	for _, nodechild := range node.childNodes {
		printTree(nodechild)    //深度优先遍历
	}
	numspace -= 4    //子节点打印完毕,收回缩进
}

打印结果:

ancient
    linear algebra
        calculus
    computer organization
        operating systems
            networks
        compilers
        programming languages
    intro to programming
        discrete math
            data structures
                operating systems
                    networks
                algorithms
                compilers
                databases
                programming languages
            formal languages
                compilers

5、广度优先遍历树

func main(){
    ......
    //拿有向图rootroot练手广度遍历
	var breadlist []string //存储广度遍历结果
	var floor []*Node      //广度遍历的一层
	floor = append(floor, rootroot)
	breadlist = breadTraverse(floor, breadlist)
	for i, node := range breadlist {
		fmt.Printf("%d %s\n", i, node)
	}

}

/*
      *
     /
   /*
  /  \
 /   *
*
 \   *
  \ /
   *
	\
	 *
*/
//数组传值,切片传引用,
func breadTraverse(floor []*Node, breadlist []string) []string {
	if len(floor) == 0 {
		return breadlist
	}
	var nextFloor []*Node //当前层floor的所有节点的下层节点
	for _, node := range floor {
		breadlist = append(breadlist, node.name) //压栈当前层节点
		//收集下一层
		for _, nodeparent := range node.childNodes {
			nextFloor = append(nextFloor, nodeparent)
		}
	}
	breadlist = breadTraverse(nextFloor, breadlist) //切片的回传是从底层到上层的
	return breadlist
}

广度优先遍历的打印结果,会给出"重复"的节点:

0 ancient
1 linear algebra
2 computer organization
3 intro to programming
4 calculus
5 compilers
6 programming languages
7 operating systems
8 discrete math
9 networks
10 data structures
11 formal languages
12 databases
13 algorithms
14 compilers
15 programming languages
16 operating systems
17 compilers
18 networks

对比有向图:

ancient
    linear algebra
        calculus
    computer organization
        operating systems
            networks
        compilers
        programming languages
    intro to programming
        discrete math
            data structures
                operating systems
                    networks
                algorithms
                compilers
                databases
                programming languages
            formal languages
                compilers

这里尤其注意Go切片的传引用,必须将处理后的形参切片回传给实参切片,否则形参切片对实参切片所做的修改可能会丢失,比如发生切片扩容时将,见上。

funcFunct(形参[]string)[]string{
形参=append(形参,"heihei")    //可能进行了扩容
return形参    //将扩容后的形参切片回传给实参
}
实参=Funct(实参)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值