线性数据结构
线性表
- 线性表,表,有序序列,放元素,是一种有序的放元素的容器,抽象概念数学概念
有序就可以编号,可以有索引 - 顺序表:使用一大块连续的内存顺序存储表中的元素,这样实现的表称为顺序表,或称连续表
- 在顺序表中,元素的关系使用顺序表的存储顺序自然地表示
- 链接表:在存储空间中将分散存储的元素链接起来,这种实现称为链接表,简称链表
顺序表
使用连续的内存单元存储该序列的所有元素,内存的顺序就是数据顺序
例如:排好的队伍,数组Array
开辟连续的内存空间,容器占用的内存称为 容量cap,可以最多容纳多少个元素
当前容器目前的元素个数,称为长度len,增加元素+1,删除元素-1,用一个元数据记录
顺序表需要开辟连续内存空间,变量指向顺序表的第一个元素地址
变量指向顺序表A = [1, 1.5, 2.1, 3, 4] A这个标识符指向数组首地址
顺序sequence不是排序sort
从CDUR四个方面来分析顺序表
C
容器元素个数 + 1
append如同排队,在队尾后增加即可
insert插队
最尾巴后插队,就是append
中间插队,占用当前位置,把当前位置与其后所有数据后移
队首插队,所有元素统统后移
挪动数据是要耗时间的,挪动的元素越多(规模越大),代价越大
D
容器元素个数 - 1
pop队尾元素移除,影响最小
remove
如果是队尾,就是pop
中间离队,后面数据统统前移
队首离队,后面数据统统前移
挪动数据是要耗时间的
U
元素个数len 不变
定位问题
更新内容
R
定位问题:
使用索引,首地址 + 该类型的字节数 * 偏移,所以,定位要用索引计算得到元素的内存地址,不用遍历,效率极高。
顺序表不管有多少个元素,都是一个四则元素公式直接推出来
使用索引计算与规模n无关,使用固定的步骤就可以计算出元素的位置,O(2) 简写为 O(1)
如果使用内容定位,内容比较,遍历的方式挨个比较内容,效率低下。最不幸,遍历完了,才知道没有
获取内容:使用索引直接定位该位置,拿走内容
遍历:容器中的元素,不管有没有顺序,我们都要不重复的将所有元素挨个摸一遍。
首地址开始,挨个偏移取内容
前提:要数据规模,如果数据规模小,随便你玩。数据规模大,都是事
顺序表适合仅仅在尾部增删
扩容问题
链接表
每一个元素存储在内存中,但是元素并不是连续的存储在内存中,散落内存的不同位置,前一个元素指向下一个元素(链接),是一种手拉手的状态。
链表是个容器,可以放元素。定义一个变量指向链表第一个元素地址即可
cap指的是当前放了多少个元素,和len一致的,不需要提前开辟内存空间
链表的长度,同顺序表
单向:前一个元素指向下一个元素
双向:前一个元素指向下一个元素,下一个元素指向上一个元素
从CDUR四个方面来分析链接表
CRUD 遍历,Create增,Read 读取,Update 改,Delete删
C
容器元素个数 + 1
尾部追加,原来的尾部指向新尾巴,容器改tail。没有数据挪动,很快
中间插入,断开原来的链接,分别和新元素拉手,没有数据挪动,代码不大
首部插入,原来的头和新头互指,容器改head,没有数据挪动,代价也不高
如果数据规模很大,请问链表新增元素代价大吗?
D
容器元素个数 - 1
尾部删除,用tail定位尾部,当前尾部的前一个成为新的tail,删除旧tail的元素
中间删除,遍历定位,当前元素删除,前驱、后继拉手即可
首部删除,用head定位首部,当前head的下一个元素成为新head,删除旧的head的元素
U
元素个数len 不变
定位问题
更新内容
R
定位问题:
使用索引,由于元素散落在内存中,不能使用顺序表公式来定位,找到开头,依次编索引。
按照遍历元素的方式来找的,但是取的每一个节点中保存的下一个地址,使用该地址定位下一个元素O(n)
和顺序表比起来,略慢,但是由于都是使用地址,效率还不错
使用内容查找定位,需要遍历查找,效率低下
获取内容:定位到了,取走内容即可
遍历,同上
我手里有一个内容,我想问该线性表中有没有?请问用什么物理存储合适?就不该使用线性表,都不好,因为是遍历。如果只有5个元素,随便你。如果规模很大呢?索引规模大就不要用线性表。用map
我手里知道索引,请问用什么线性表存储?顺序表更快定位
我手里有索引,我要使用索引插入数据或删除数据,用什么?链接表更合适。如果删除或增加都集中在尾部,更适合用顺序表,但是数据增加较多的话,顺序表开始扩容较为频繁,不如使用链接表。
操作是增删随意且需要经常使用索引定位数据,用什么?如果数据规模小,且增删较少,使用索引定位多,可以考虑使用顺序表。但是规模大增删操作随意且多,就只能使用链接表。
数组
容器不可变,容器的元素个数不能变了。数组必须给出长度,以后数组这个容器不能增删元素了,不能扩容了。长度是死的,必须定义时给出
定义数组
func main() {
// 注意下面2种区别
var a0 [3]int // 零值初始化3个元素的数组
var a1 = [3]int{} // 零值初始化3个元素的数组
fmt.Println(a0, a1)
}
[0 0 0] [0 0 0]
func main() {
var a2 [3]int = [3]int{1, 3, 5} // 声明且初始化,不推荐,啰嗦
var a3 = [3]int{1, 3, 5} // 声明且初始化,推荐
fmt.Println(a2, a3)
}
[1 3 5] [1 3 5]
package main
import "fmt"
func main() {
a5 := [...]int{10, 30, 50} // ...让编译器确定当前数组大小
fmt.Printf("%T %[1]d", a5)
}
[3]int [10 30 50]
func main() {
a6 := [5]int{100, 200} // 顺序初始化前面的,其余用零值填充
a7 := [5]int{1: 300, 3: 400} // 指定索引位置初始化,其余用零值填充
fmt.Println(a6, a7)
}
[100 200 0 0 0] [0 300 0 400 0]
二维数组
func main() {
a8 := [2][3]int{{100}} // 两行三列
fmt.Println(a8)
}
[[100 0 0] [0 0 0]]
// 多维数组,只有第一维才能用...推测
// 第一维有4个,第二维有3个。可以看做4行3列的表
func main() {
a9 := [...][3]int{{10}, {11, 12}, {13, 14, 15}, {16}}
fmt.Println(a9)
}
[[10 0 0] [11 12 0] [13 14 15] [16 0 0]]
长度和容量
cap即capacity,容量,表示给数组分配的内存空间可以容纳多少个元素
len即length,长度,指的是容器中目前有几个元素
由于数组创建时就必须确定的元素个数,且不能改变长度,所以不需要预留多余的内存空间,因此cap和len对数组来说相等
索引
Go语言不支持负索引。通过[index]来获取该位置上的值。索引范围就是[0, 长度-1]
0到len(array)-1,使用索引是最快定位方式了
修改
func main() {
a10 := [...]int{10, 30, 50}
a10[0] = 100
fmt.Println(a10)
}
[100 30 50]
func main() {
a10 := [...]int{10, 30, 50}
a10[0] += 100
fmt.Println(a10)
}
[110 30 50]
遍历
索引遍历
func main() {
a11 := [...]int{10, 20, 30}
for i := 0; i < len(a11); i++ {
fmt.Println(i, a11[i])
}
}
0 10
1 20
2 30
for-range遍历
func main() {
a11 := [...]int{10, 20, 30}
for i, v := range a11 { // i是索引,v是元素值
fmt.Println(i, v, a11[i])
}
}
0 10 10
1 20 20
2 30 30
func main() {
a11 := [...]int{10, 20, 30}
for i := range a11 { // i是索引
fmt.Println(i, a11[i])
}
}
0 10
1 20
2 30
内存模型
func main() {
var a [3]int // 内存开辟空间存放长度为3的数组,零值填充
for i := 0; i < len(a); i++ {
fmt.Println(i, a[i], &a[i])
}
}
0 0 0xc000012150
1 0 0xc000012158
2 0 0xc000012160
fmt.Printf("%p %p %v\n", &a, &a[0], a)
0xc000012150 0xc000012150 [0 0 0]
a[0] = 1000
fmt.Printf("%p %p, %v\n", &a, &a[0], a)
0xc000126060 0xc000126060, [1000 0 0]
结论:数组必须在编译时就确定长度,之后不能改变长度
*数组首地址就是数组地址*
所有元素一个接一个顺序存储在内存中
元素的值可以改变,但是元素地址不变
上面每个元素间隔8个字节,正好64位,符合int类型定义
字符串类型
var a = [3]string{"abc", "def", "xyz"} // 内存开辟空间存放长度为3的数组
for i := 0; i < len(a); i++ {
fmt.Println(i, a[i], &a[i])
}
fmt.Printf("%p %p, %v\n", &a, &a[0], a)
a[0] = "oooooo"
fmt.Printf("%p %p, %v\n", &a, &a[0], a)
运行结果
0 abc 0xc000138480
1 def 0xc000138490
2 xyz 0xc0001384a0
0xc000138480 0xc000138480, [abc def xyz]
0xc000138480 0xc000138480, [oooooo def xyz]
每个元素间隔16个字节,为什么?"abc"是几个字节?这说明什么?
字符串字面常量,一旦定义不可改变,不同的字符串长度不一,数组采用string为元素,间隔怎么办?测试后发现元素存储空间间隔一样,都是16字节,说明字符串复杂一些。16个字节里面放了一个指针,指向字符串的内存位置