這是第三週的練習題,原本應該先發第二週的,因為週末的時候,我的母親大人來看望她的寶貝兒子,哈哈,我得帶她看看廈門這座美麗的城市呀。
這兩天我抓緊整理下第二週的題目和答案,下面我把之前的也列出來:
本週練習內容:資料結構與演算法 —— LinkedList
這些都是資料結構與演算法,一部分方法是團隊其他成員實現的,一部分我自己做的,有什麼其他實現方法或錯誤,歡迎各位大佬指點,感謝。
一、連結串列是什麼?與陣列有什麼區別?生活中有什麼案例?
解析:
概念參考閱讀 連結串列 —— 維基百科
1.概念:
連結串列(Linked list)是一種上一個元素的引用指向下一個元素的儲存結構,連結串列通過指標來連線元素與元素;
連結串列是線性表的一種,所謂的線性表包含順序線性表和連結串列,順序線性表是用陣列實現的,在記憶體中有順序排列,通過改變陣列大小實現。而連結串列不是用順序實現的,用指標實現,在記憶體中不連續。意思就是說,連結串列就是將一系列不連續的記憶體聯絡起來,將那種碎片記憶體進行合理的利用,解決空間的問題。
所以,連結串列允許插入和刪除表上任意位置上的節點,但是不允許隨即存取。連結串列有很多種不同的型別:單向連結串列、雙向連結串列及迴圈連結串列。
2.與陣列的區別:
-
相同:
兩種結構均可實現資料的順序儲存,構造出來的模型呈線性結構。 -
不同:
連結串列是鏈式的儲存結構;陣列是順序的儲存結構。
連結串列通過指標來連線元素與元素,陣列則是把所有元素按次序依次儲存。
連結串列的插入刪除元素相對陣列較為簡單,不需要移動元素,且較為容易實現長度擴充,但是尋找某個元素較為困難。
陣列尋找某個元素較為簡單,但插入與刪除比較複雜,由於最大長度需要再程式設計一開始時指定,故當達到最大長度時,擴充長度不如連結串列方便。
陣列和連結串列一些操作的時間複雜度對比:
陣列:
- 查詢複雜度:O(1)
- 新增/刪除複雜度:O(n)
連結串列:
- 查詢複雜度:O(n)
- 新增/刪除複雜度:O(1)
3.生活中的案例:
火車,是由一些列車廂連線起來;
尋寶遊戲,每個線索都是下一個線索地點的指標。
二、請事先一個連結串列,並實現以下方法
append(element)
:向列表尾部新增一個新的元素。insert(position, element)
:向列表指定位置插入一個新的元素。remove(element)
:從列表中移除並返回特定元素(若有多個相同元素則取第一次出現的情況)。indexOf(element)
:返回元素在列表的索引(若有多個相同元素則取第一次出現的情況),如果列表中沒有該元素則返回-1
。removeAt(position)
:從列表中,移除並返回特定位置的一項。isEmpty()
:如果列表不含任何元素,返回true
,否則返回false
。size()
:返回列表中元素個數,與陣列的length
屬性類似。toString()
:由於列表項使用Node
類,需要重寫繼承自 JavaScript 物件預設的toString()
方法,讓其只輸出元素的值。
提示:Web 端優先使用 ES6 以上的語法實現。
解析:
class Node {
constructor(element){
this.element = element
this.next = null
}
}
class LinkedList {
constructor(){
this.length = 0
this.head = null
}
/**
* 新增元素(末尾新增)
* @param {*} element 新增的元素
*/
append(element){
let node = new Node(element)
if(!this.head){
this.head = node
}else{
let current = this.head
// 查詢最後一項
while(current.next){
current = current.next
}
// 將最後一下的next賦值為node,實現追加元素
current.next = node
}
this.length ++
}
/**
* 新增元素(指定位置)
* @param {Number} position 新增的位置
* @param {*} element 新增的元素
*/
insert(position, element){
if(position >= 0 && position <= this.length){
let node = new Node(element),
index = 0,
previous = null
if(position === 0){
node.next = this.head
this.head = node
}else{
let current = this.head
while(index++ < position){
previous = current
current = current.next
}
previous.next = node
node.next = current
}
this.length ++
}
}
/**
* 刪除元素
* @param {*} element 刪除的元素
* @return {*} 被刪除的元素
*/
remove(element){
let current = this.head,
previous = null
if(element === this.head.element){
this.head = current.next
}else{
while(current.next && current.element !== element){
previous = current
current = current.next
}
previous.next = current.next
this.length --
return current.element
}
}
/**
* 刪除元素(指定位置)
* @param {Number} position 刪除元素的位置
* @return {*} 被刪除的元素
*/
removeAt(position){
if(position >= 0 && position <= this.length){
let current = this.head,
index = 0,
previous = null
if(position === 0){ // 刪除第一項
this.head = current.next
}else{
while(index++ < position){
previous = current
current = current.next
}
previous.next = current.next
}
this.length --
return current.element
}
}
/**
* 查詢指定元素的位置
* @param {*} element 查詢的元素
* @return {Number} 查詢的元素的下標
*/
indexOf(element){
let current = this.head,
index = 0
while(current.next && current.element !== element){
current = current.next
index ++
}
return index === 0 ? -1 : index
}
/**
* 連結串列是否為空
* @return {Boolean}
*/
isEmpty(){
return this.length === 0
}
/**
* 連結串列的長度
* @return {Number}
*/
size(){
return this.length
}
/**
* 將連結串列轉成字串
* @return {String}
*/
toString(){
let current = this.head,
arr = new Array()
while(current.next){
arr.push(current.element)
current = current.next
}
arr.push(current.element)
return arr.toString()
}
}
let leo = new LinkedList()
leo.append(3)
leo.append(6)
leo.append(9)
console.log(leo.length)
console.log(leo.head)
leo.remove(6)
console.log(leo.length)
console.log(leo.head)
console.log(leo.toString())
複製程式碼
三、實現反轉連結串列
用連結串列的方式,輸出一個反轉後的單連結串列。
示例:
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
// input
let head = {
'val': 1,'next': {
'val': 2,'next': {
'val': 3,'next': {
'val': 4,'next': {
'val': 5,
'next': null
}
}
}
}
};
reverseList(head)
// output
head = {
'val': 5,'next': {
'val': 4,'next': {
'val': 3,'next': {
'val': 2,'next': {
'val': 1,
'next': null
}
}
}
}
};
複製程式碼
解題思路1.使用迭代:
在遍歷列表時,將當前節點的 next
指標改為指向前一個元素。由於節點沒有引用其上一個節點,因此必須先儲存其前一個元素。在更改引用之前,還需要另一個指標來儲存下一個節點。不要忘記在最後返回新的頭引用!
解題思路2.使用遞迴:
通過遞迴修改 head.next.next
和 head.next
指標來實現。
解析:
題目出自:Leetcode 206. 反轉連結串列
介紹兩種常用方法:
1.使用迭代:
在遍歷列表時,將當前節點的 next
指標改為指向前一個元素。由於節點沒有引用其上一個節點,因此必須先儲存其前一個元素。在更改引用之前,還需要另一個指標來儲存下一個節點。不要忘記在最後返回新的頭引用!
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
let reverseList = function(head) {
let pre = null, curr = head
while (curr) {
next = curr.next
curr.next = pre
pre = curr
curr = next
}
return pre
};
複製程式碼
複雜度分析
時間複雜度:O(n)
。 假設 n
是列表的長度,時間複雜度是 O(n)
。
空間複雜度:O(1)
。
2.使用遞迴:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
let reverseList = function(head) {
if(head == null || head.next == null) return head
let pre = reverseList(head.next)
head.next.next = head
head.next = null
return pre
};
複製程式碼
複雜度分析
時間複雜度:O(n)
。 假設 n
是列表的長度,那麼時間複雜度為 O(n)
。
空間複雜度:O(n)
。 由於使用遞迴,將會使用隱式棧空間。遞迴深度可能會達到 n
層。
四、判斷連結串列是否有環
設計一個函式 hasCycle
,接收一個連結串列作為引數,判斷連結串列中是否有環。
為了表示給定連結串列中的環,我們使用整數 pos
來表示連結串列尾連線到連結串列中的位置(索引從 0
開始)。 如果 pos
是 -1
,則在該連結串列中沒有環。
需要注意的是,不可能存在多個環,最多隻有一個。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:連結串列中有一個環,其尾部連線到第二個節點。
複製程式碼
示例 2:
輸入:head = [1,2], pos = 0
輸出:true
解釋:連結串列中有一個環,其尾部連線到第一個節點。
複製程式碼
示例 3:
輸入:head = [1], pos = -1
輸出:false
解釋:連結串列中沒有環。
複製程式碼
解題思路1.判斷是否有 null:
一直遍歷下去,如果遍歷到 null
則表示沒有環,否則有環,但是考慮到效能問題,最好給定一段時間作為限制,超過時間就不要繼續遍歷。
解題思路2.標記法:
也是要遍歷每個節點,並在遍歷的節點新增標記,如果後面遍歷過程中,遇到有這個標記的節點,即表示有環,反之沒有環。
解題思路3.使用雙指標(龜兔賽跑式):
設定2個指標,一個 快指標
每次走 2 步,慢指標
每次走 1 步,如果沒有環的情況,最後這兩個指標不會相遇,如果有環,會相遇。
解析:
題目出自:Leetcode 141. 環形連結串列
1.斷是否有 null
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
let hasCycle = function(head) {
while(head){
if(head.value == null) return true
head.value = null
head = head.next
}
return false
}
複製程式碼
2.標記法
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
let hasCycle = function(head) {
let node = head
while(node){
if(node.isVisit){
return true
}else{
node.isVisit = true
}
node = node.next
}
return false
};
複製程式碼
3.使用雙指標
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
let hasCycle = function(head) {
if(head == null || head.next == null) return false
let slow = head, fast = head.next
while(slow != fast){
if(fast == null || fast.next == null) return false
slow = slow.next // 慢指標每次走1步
fast = fast.next.next // 快指標每次走1補
}
return true
};
複製程式碼
五、實現兩兩交換連結串列中的節點
給定一個連結串列,兩兩交換其中相鄰的節點,並返回交換後的連結串列。
你不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。
示例:
給定 1->2->3->4, 你應該返回 2->1->4->3.
給定 1->2->3->4->5, 你應該返回 2->1->4->3->5.
複製程式碼
解題思路1.使用迭代:
和反轉連結串列類似,關鍵在於有三個指標,分別指向前後和當前節點,而不同在於兩兩交換後,移動節點的步長為2,需要注意。
解題思路2.使用遞迴:
這裡也可以使用遞迴,也可以參考反轉連結串列的問題,終止條件是遞迴到連結串列為空,或者只剩下一個元素沒得交換了,才終止。
解析:
題目出自:Leetcode 24. 兩兩交換連結串列中的節點
介紹兩種常用方法:
1.使用迭代:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
let swapPairs = function (head){
if(!head) return null
let arr = []
while(head){
let next = head.next
head.next = null
arr.push(head)
head = next
}
for(let i = 0; i < arr.length; i += 2){
let [a, b] = [arr[i], arr[i + 1]]
if(!b) continue
[arr[i], arr[i + 1]] = [b, a]
}
for(let i = 0; i < arr.length - 1; i ++){
arr[i].next = arr[i + 1]
}
return arr[0]
}
複製程式碼
2.使用遞迴:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
let swapPairs = function (head){
if(head == null || head.next ==null) return head
let next = head.next
head.next = swapPairs(next.next)
next.next = head
return next
}
複製程式碼