數據結構之線性表--鏈式存儲結構--單鏈表

定義

線性表的鏈式存儲結構,是用一組任意的存儲單元來存儲線性表的數據元素,這些單元能夠分散在內存中的任意位置,即不要求邏輯上相鄰的兩個元素在物理上也相鄰;而是經過「鏈」創建起數據元素之間的邏輯關係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!

相關文章
相關標籤/搜索