数据结构之线性表--链式存储结构--单链表

定义

线性表的链式存储结构,是用一组任意的存储单元来存储线性表的数据元素,这些单元能够分散在内存中的任意位置,即不要求逻辑上相邻的两个元素在物理上也相邻;而是经过“链”创建起数据元素之间的逻辑关系node

  • 因为在物理上不必定相邻,所以每一个数据元素,除了存储自己的信息以外,还须要存储指示其直接后继的信息;
  • 存储数据元素信息的域称为数据域;
  • 存储直接后继位置的域称为指针域,其中的信息称为指针或链;
  • 数据域和指针域组合起来,称为结点;
  • n个结点连接成一个链表,即为线性表(a1, a2, a3, …, an)的链式存储结构;
  • 通常状况下,链表中每一个结点能够包含若干个数据域和若干个指针域。若是每一个结点中只包含一个指针域,则称其为单链表
  • 链表中的第一个结点(或者头节点)的存储位置叫作头指针,最后一个结点指针为空(NULL);
  • 为了便于实现各类操做,能够在单链表的第一个结点以前增设一个结点,称为头结点。

单链表用go语言描述以下:

type data interface{}

type Node struct {
	Data data // 数据域
	Next *Node // 指针域
}

type LinkList struct {
	Head *Node
	len int
}

// 初始化一个链表
func New() *LinkList {
	l := &LinkList{Head: &Node{}}
	l.len = 0
	return l
}
复制代码

主要操做

查找

查找分为按值查找,和按序号查找,不过在算法的思想上基本是一致的:算法

一、从表头开始找,判断当前节点是否知足查找条件;ui

二、若是不知足,则将指针后移一位,指向下一个结点,继续判断条件;spa

三、找到知足查找条件的结点,则退出循环,返回该结点,若是没找到,则返回null指针

// 按序号查找
func (l *LinkList) FindKth(k int) *Node {
	if k < 1 || k > l.len {
		return nil
	}
	current := l.Head
	for i := 1; i <= k; i++ {
		current = current.Next
	}
	return current
}

// 按值查找
func (l *LinkList) Find(value data) *Node {
	for current := l.Head; current != nil; current = current.Next {
		if current.Data == value {
			return current
		}
	}
	return nil
}
复制代码
  • 两个算法的时间复杂度为O(n)
  • 循环中都使用到了“工做指针后移”,这也是不少算法的经常使用技术

插入

在第i-1(1<=i<=n+1)个结点以后插入一个值为X的新结点,算法思想:code

一、构建一个新的结点s;cdn

二、找到第i-1个结点p;blog

三、修改指针,插入新的结点。内存

其中第3步,咱们用图表示: it

上图的操做能够得出下面两行代码

s.Next = p.Next // 1处创建连接
p.Next = s // 2处创建连接
复制代码

若是将这两行代码的顺序交换一下会怎么样?

先执行p.Next = s,这个时候就p.Next指向了s结点,而后执行s.Next = p.Next,可是p.Next已是s结点了,所以也就变成了s.Next = s。这个时候插入就会失败。因此这两句是不管如何不能弄反的。

func (l *LinkList) Insert(value data, i int) bool {
	preNode := l.FindKth(i - 1)
	if preNode == nil {
		return false
	}
	node := &Node{Data: value}
	node.Next = preNode.Next
	preNode.Next = node
	l.len++
	return true
}
复制代码
  • 算法的时间复杂度取决了i位置,所以为O(n)

删除

删除链表的第i(1<=i<=n)个位置的结点,算法思想:

一、找到第i-1个结点,为p;

二、用s保存p.Next的结点,即第i个结点;

三、将p.Next指向s.Next,断开结点的连接;

四、用e保存s的值,释放s结点,返回e。

func (l *LinkList) Delete(i int) (data, bool) {
	preNode := l.FindKth(i - 1)
	if preNode == nil {
		return nil, false
	}
	deleteNode := preNode.Next
	preNode.Next = deleteNode.Next
	value := deleteNode.Data
	deleteNode = nil
	l.len--
	return value, true
}
复制代码
  • 算法的时间复杂度取决了i位置,所以为O(n)

整表建立

咱们可使用头插法,或者尾插法的方式,建立链表。

头插法

即在建立链表时,每一个元素都按顺序的插在表头。

一、给链表添加一个在表头插入一个元素的方法,称为InsertHead;

二、依次使用InsertHead将元素加入链表中。

func (l *LinkList) InsertHead(value data) {
	node := &Node{Data: value}
	node.Next = l.Head.Next
	l.Head.Next = node
	l.len++
}

// 头插法建立
l := LinkList.New()
for i := 1; i <= 5; i++ {
    // 将1到5依次插入表头
    l.InsertHead(i)
}
复制代码

查看链表的结构:

能够看的出来,使用头插法建立的链表,存储的顺序是反向的。

尾插法

即在建立链表时,每一个元素都按顺序的插在表尾。

一、给链表添加一个在表头插入一个元素的方法,InsertTail;

二、依次使用InsertTail将元素加入链表中。

func (l *LinkList) InsertTail(value data) {
	node := &Node{Data: value}
	current := l.Head
	for current.Next != nil {
		current = current.Next
	}
	current.Next = node
	l.len++
}

// 尾插法建立
l := LinkList.New()
for i := 1; i <= 5; i++ {
    // 将1到5依次插入表尾
	l.InsertTail(i)
}
复制代码

链表结构:

总结

咱们从时间和空间上对比一下线性表的链式存储与顺序存储:

时间

查找:

  • 顺序存储结构O(1)
  • 单链表O(n)

插入和删除:

  • 顺序存储结构须要平均移动表长一半的元素,时间为O(n)
  • 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
  • 好比,在第i个位置,连续插入10个元素,对于顺序存储,每次插入都要移动后面的元素,因此每次都是O(n)
  • 而单链表,只有在第一次的时候要找到i位置,即O(n),以后的插入都是O(1)

空间

  • 顺序存储结构须要预先分配空间,若是分配大了,会形成空间浪费,若是分配小了,可能产生溢出
  • 单链表不须要预先分配空间,只要还用空间就能够进行分配,元素个数也不受限制

结语

  • 若是线性表须要频繁查找,不多进行插入和删除,则适合用顺序存储;
  • 若是须要频繁的插入和删除,则适合用单链表;
  • 若是事先知道线性表的长度,则适合使用顺序存储,反之,可使用单链表;
  • 没有银弹——总之,二者各有优缺点,咱们应当根据实际状况,选择合适的存储结构。

Thanks!

相关文章
相关标签/搜索