可变长数组

因为数组大小是固定的,当数据元素特别多时,固定的数组无法储存这么多的值,所以可变长数组出现了,这也是一种数据结构。在Golang语言中,可变长数组被内置在语言里面:切片slice

slice是对底层数组的抽象和控制。它是一个结构体:

type slice struct {
array unsafe.Pointer
len int
cap int
}
  1. 指向底层数组的指针。(Golang语言是没有操作原始内存的指针的,所以unsafe包提供相关的对内存指针的操作,一般情况下非专业人员勿用)
  2. 切片的真正长度,也就是实际元素占用的大小。
  3. 切片的容量,底层固定数组的长度。

每次可以初始化一个固定容量的切片,切片内部维护一个固定大小的数组。当append新元素时,固定大小的数组不够时会自动扩容,如:

package main

import "fmt"

func main() {
// 创建一个容量为2的切片
array := make([]int, 0, 2)
fmt.Println("cap", cap(array), "len", len(array), "array:", array) // 虽然 append 但是没有赋予原来的变量 array
_ = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
_ = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
_ = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array) fmt.Println("-------") // 赋予回原来的变量
array = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
array = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
array = append(array, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
array = append(array, 1, 1, 1, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
array = append(array, 1, 1, 1, 1, 1, 1, 1, 1, 1)
fmt.Println("cap", cap(array), "len", len(array), "array:", array)
}

输出:

cap 2 len 0 array: []
cap 2 len 0 array: []
cap 2 len 0 array: []
cap 2 len 0 array: []
-------
cap 2 len 1 array: [1]
cap 2 len 2 array: [1 1]
cap 4 len 3 array: [1 1 1]
cap 8 len 7 array: [1 1 1 1 1 1 1]
cap 16 len 16 array: [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

我们可以看到Golang的切片无法原地append,每次添加元素时返回新的引用地址,必须把该引用重新赋予之前的切片变量。并且,当容量不够时,会自动倍数递增扩容。事实上,Golang在切片长度大于1024后,会以接近于1.25倍进行容量扩容。

具体可参考标准库runtime下的slice.go文件。

一、实现可变长数组

我们来实现一个简单的,存放整数的,可变长的数组版本。

因为Golang的限制,不允许使用[n]int来创建一个固定大小为n的整数数组,只允许使用常量来创建大小。

所以我们这里会使用切片的部分功能来代替数组,虽然切片本身是可变长数组,但是我们不会用到它的append功能,只把它当数组用。

import (
"sync"
) // 可变长数组
type Array struct {
array []int // 固定大小的数组,用满容量和满大小的切片来代替
len int // 真正长度
cap int // 容量
lock sync.Mutex // 为了并发安全使用的锁
}

1.1. 初始化数组

创建一个len个元素,容量为cap的可变长数组:

// 新建一个可变长数组
func Make(len, cap int) *Array {
s := new(Array)
if len > cap {
panic("len large than cap")
} // 把切片当数组用
array := make([]int, cap, cap) // 元数据
s.array = array
s.cap = cap
s.len = 0
return s
}

主要利用满容量和满大小的切片来充当固定数组,结构体Array里面的字段lencap来控制值的存取。不允许设置len > cap的可变长数组。

时间复杂度为:O(1),因为分配内存空间和设置几个值是常数时间。

1.2. 添加元素

// 增加一个元素
func (a *Array) Append(element int) {
// 并发锁
a.lock.Lock()
defer a.lock.Unlock() // 大小等于容量,表示没多余位置了
if a.len == a.cap {
// 没容量,数组要扩容,扩容到两倍
newCap := 2 * a.len // 如果之前的容量为0,那么新容量为1
if a.cap == 0 {
newCap = 1
} newArray := make([]int, newCap, newCap) // 把老数组的数据移动到新数组
for k, v := range a.array {
newArray[k] = v
} // 替换数组
a.array = newArray
a.cap = newCap } // 把元素放在数组里
a.array[a.len] = element
// 真实长度+1
a.len = a.len + 1 }

首先添加一个元素到可变长数组里,会加锁,这样会保证并发安全。然后将值放在数组里:a.array[a.len] = element,然后len + 1,表示真实大小又多了一个。

当真实大小len = cap时,表明位置都用完了,没有多余的空间放新值,那么会创建一个固定大小2*len的新数组来替换老数组:a.array = newArray,当然容量也会变大:a.cap = newCap。如果一开始设置的容量cap = 0,那么新的容量会是从 1 开始。

添加元素中,耗时主要在老数组中的数据移动到新数组,时间复杂度为:O(n)。当然,如果容量够的情况下,时间复杂度会变为:O(1)

如何添加多个元素:

// 增加多个元素
func (a *Array) AppendMany(element ...int) {
for _, v := range element {
a.Append(v)
}
}

只是简单遍历一下,调用Append函数。其中...intGolang的语言特征,表示多个函数变量。

1.3. 获取指定下标元素

// 获取某个下标的元素
func (a *Array) Get(index int) int {
// 越界了
if a.len == 0 || index >= a.len {
panic("index over len")
}
return a.array[index]
}

当可变长数组的真实大小为0,或者下标index超出了真实长度len,将会panic越界。

因为只获取下标的值,所以时间复杂度为O(1)

1.4. 获取真实长度和容量

// 返回真实长度
func (a *Array) Len() int {
return a.len
} // 返回容量
func (a *Array) Cap() int {
return a.cap
}

时间复杂度为O(1)

1.5. 示例

现在我们来运行完整的可变长数组的例子:

package main

import (
"fmt"
"sync"
) // 可变长数组
type Array struct {
array []int // 固定大小的数组,用满容量和满大小的切片来代替
len int // 真正长度
cap int // 容量
lock sync.Mutex // 为了并发安全使用的锁
} // 新建一个可变长数组
func Make(len, cap int) *Array {
s := new(Array)
if len > cap {
panic("len large than cap")
} // 把切片当数组用
array := make([]int, cap, cap) // 元数据
s.array = array
s.cap = cap
s.len = 0
return s
} // 增加一个元素
func (a *Array) Append(element int) {
// 并发锁
a.lock.Lock()
defer a.lock.Unlock() // 大小等于容量,表示没多余位置了
if a.len == a.cap {
// 没容量,数组要扩容,扩容到两倍
newCap := 2 * a.len // 如果之前的容量为0,那么新容量为1
if a.cap == 0 {
newCap = 1
} newArray := make([]int, newCap, newCap) // 把老数组的数据移动到新数组
for k, v := range a.array {
newArray[k] = v
} // 替换数组
a.array = newArray
a.cap = newCap } // 把元素放在数组里
a.array[a.len] = element
// 真实长度+1
a.len = a.len + 1 } // 增加多个元素
func (a *Array) AppendMany(element ...int) {
for _, v := range element {
a.Append(v)
} } // 获取某个下标的元素
func (a *Array) Get(index int) int {
// 越界了
if a.len == 0 || index >= a.len {
panic("index over len")
}
return a.array[index]
} // 返回真实长度
func (a *Array) Len() int {
return a.len
} // 返回容量
func (a *Array) Cap() int {
return a.cap
} // 辅助打印
func Print(array *Array) (result string) {
result = "["
for i := 0; i < array.Len(); i++ {
// 第一个元素
if i == 0 {
result = fmt.Sprintf("%s%d", result, array.Get(i))
continue
} result = fmt.Sprintf("%s %d", result, array.Get(i))
}
result = result + "]"
return
} func main() {
// 创建一个容量为3的动态数组
a := Make(0, 3)
fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // 增加一个元素
a.Append(10)
fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // 增加一个元素
a.Append(9)
fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // 增加多个元素
a.AppendMany(8, 7)
fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))
}

将打印出:

cap 3 len 0 array: []
cap 3 len 1 array: [10]
cap 3 len 2 array: [10 9]
cap 6 len 4 array: [10 9 8 7]

可以看到,容量会自动翻倍。

二、总结

可变长数组在实际开发上,经常会使用到,其在固定大小数组的基础上,会自动进行容量扩展。

因为这一数据结构的使用频率太高了,所以,Golang自动提供了这一数据类型:切片(可变长数组)。大家一般开发过程中,直接使用这一类型即可。

系列文章入口

我是陈星星,欢迎阅读我亲自写的 数据结构和算法(Golang实现),文章首发于 阅读更友好的GitBook

数据结构和算法(Golang实现)(13)常见数据结构-可变长数组的更多相关文章

  1. 数据结构和算法(Golang实现)(11)常见数据结构-前言

    常见数据结构及算法 数据结构主要用来组织数据,也作为数据的容器,载体. 各种各样的算法,都需要使用一定的数据结构来组织数据. 常见的典型数据结构有: 链表 栈和队列 树 图 上述可以延伸出各种各样的术 ...

  2. 数据结构和算法(Golang实现)(12)常见数据结构-链表

    链表 讲数据结构就离不开讲链表.因为数据结构是用来组织数据的,如何将一个数据关联到另外一个数据呢?链表可以将数据和数据之间关联起来,从一个数据指向另外一个数据. 一.链表 定义: 链表由一个个数据节点 ...

  3. 数据结构和算法(Golang实现)(14)常见数据结构-栈和队列

    栈和队列 一.栈 Stack 和队列 Queue 我们日常生活中,都需要将物品排列,或者安排事情的先后顺序.更通俗地讲,我们买东西时,人太多的情况下,我们要排队,排队也有先后顺序,有些人早了点来,排完 ...

  4. 数据结构和算法(Golang实现)(15)常见数据结构-列表

    列表 一.列表 List 我们又经常听到列表 List数据结构,其实这只是更宏观的统称,表示存放数据的队列. 列表List:存放数据,数据按顺序排列,可以依次入队和出队,有序号关系,可以取出某序号的数 ...

  5. 数据结构和算法(Golang实现)(16)常见数据结构-字典

    字典 我们翻阅书籍时,很多时候都要查找目录,然后定位到我们要的页数,比如我们查找某个英文单词时,会从英语字典里查看单词表目录,然后定位到词的那一页. 计算机中,也有这种需求. 一.字典 字典是存储键值 ...

  6. 数据结构和算法(Golang实现)(17)常见数据结构-树

    树 树是一种比较高级的基础数据结构,由n个有限节点组成的具有层次关系的集合. 树的定义: 有节点间的层次关系,分为父节点和子节点. 有唯一一个根节点,该根节点没有父节点. 除了根节点,每个节点有且只有 ...

  7. 数据结构和算法(Golang实现)(25)排序算法-快速排序

    快速排序 快速排序是一种分治策略的排序算法,是由英国计算机科学家Tony Hoare发明的, 该算法被发布在1961年的Communications of the ACM 国际计算机学会月刊. 注:A ...

  8. 数据结构和算法(Golang实现)(1)简单入门Golang-前言

    数据结构和算法在计算机科学里,有非常重要的地位.此系列文章尝试使用 Golang 编程语言来实现各种数据结构和算法,并且适当进行算法分析. 我们会先简单学习一下Golang,然后进入计算机程序世界的第 ...

  9. 数据结构和算法(Golang实现)(2)简单入门Golang-包、变量和函数

    包.变量和函数 一.举个例子 现在我们来建立一个完整的程序main.go: // Golang程序入口的包名必须为 main package main // import "golang&q ...

随机推荐

  1. 《Python学习手册 第五版》 -第15章 文档

    本章主要介绍Python中的文档,会通过多种方式来说明,如果查看Python自带文档和其他参考的资料 本章重点内容 1.#注释:源文件文档 2.dir函数:以列表显示对象中可用的属性 3.文档字符串 ...

  2. CKafka如何助力腾讯课堂实现百万消息稳定互动?

    疫情期间,为了保障国内学子的正常学习进度,腾讯课堂积极响应国家“停工不停学”的号召,紧急上线疫情期间专用的“老师极速版”,使广大师生足不出户,即可快速便捷的完成线上开课.面对线上课堂百万量级的互动消息 ...

  3. Linux软件安装之JDK的安装

    JDK的安装 1.1. 下载JDK,此处版本是1.8u131,实际操作以自己具体版本为准 先查看Linux系统是多少位(32位/64位):getconf LONG_BIT 然后去官网下载JDK [jd ...

  4. IE浏览器下载文件中文文件名乱码问题解决

    处理过程 根据IE的F12中的log提示,是因为http头信息中的编码替换了html文件中的编码.我最初的思路是设置Tomcat默认编码,但是我发现我已经在Server.xml中设置过,想到这里我想到 ...

  5. 【2019多校第一场补题 / HDU6582】2019多校第一场E题1005Path——最短路径+网络流

    HDU6582链接 题意 在一张有向图中,有一个起点和一个终点,你需要删去部分路径,使得起点到终点的最短距离增加(并不要求需要使得距离变成最大值),且删除的路径长度最短.求删去的路径总长为多少 分析 ...

  6. PHP - json_decode returns NULL的解决办法

    碰到了PHP json_decode returns NULL, 肿么办? 1. google 一下, 关键字:PHP json_decode NULL 首先你能看到我这个这个帖子:) http:// ...

  7. NOI ONLINE 提高组 序列 根据性质建图

    题目链接 https://www.luogu.com.cn/problem/P6185 题意 应该不难懂,跳过 分析 说实话第一眼看到这题的时候我有点懵,真不知道怎么做,不过一看数据,还好还好,暴力能 ...

  8. TensorFlow-Bitcoin-Robot:一个基于 TensorFlow LSTM 模型的 Bitcoin 价格预测机器人。

    简介 TensorFlow-Bitcoin-Robot:一个基于 TensorFlow LSTM 模型的 Bitcoin 价格预测机器人. 文章包括一下几个部分: 1.为什么要尝试做这个项目? 2.为 ...

  9. AI领域:如何做优秀研究并写高水平论文?

    来源:深度强化学习实验室 每个人从本科到硕士,再到博士.博士后,甚至工作以后,都会遇到做研究.写论文这个差事.论文通常是对现有工作的一个总结和展示,特别对于博士和做研究的人来说,论文则显得更加重要. ...

  10. 调试 node.js 程序

    调试 node.js 程序 在程序开发中,如何快速的查找定位问题是一项非常重要的基本功.在实际开发过程中,或多或少都会遇到程序出现问题导致无法正常运行的情况,因此,调试代码就变成了一项无法避免的工作. ...