這是第三週的練習題,本來應該先發第二週的,由於週末的時候,個人母親大人來看望她的寶貝兒子,哈哈,我得帶她看看廈門這座美麗的城市呀。前端
這兩天我抓緊整理下第二週的題目和答案,下面我把以前的也列出來:node
本週練習內容:數據結構與算法 —— LinkedListgithub
這些都是數據結構與算法,一部分方法是團隊其餘成員實現的,一部分我本身作的,有什麼其餘實現方法或錯誤,歡迎各位大佬指點,感謝。算法
解析:
概念參考閱讀 鏈表 —— 維基百科編程
1.概念:數組
鏈表(Linked list)是一種上一個元素的引用指向下一個元素的存儲結構,鏈表經過指針來鏈接元素與元素;bash
鏈表是線性表的一種,所謂的線性表包含順序線性表和鏈表,順序線性表是用數組實現的,在內存中有順序排列,經過改變數組大小實現。而鏈表不是用順序實現的,用指針實現,在內存中不連續。意思就是說,鏈表就是將一系列不連續的內存聯繫起來,將那種碎片內存進行合理的利用,解決空間的問題。微信
因此,鏈表容許插入和刪除表上任意位置上的節點,可是不容許隨即存取。鏈表有不少種不一樣的類型:單向鏈表、雙向鏈表及循環鏈表。數據結構
2.與數組的區別:
相同:
兩種結構均可實現數據的順序存儲,構造出來的模型呈線性結構。
不一樣:
鏈表是鏈式的存儲結構;數組是順序的存儲結構。
鏈表經過指針來鏈接元素與元素,數組則是把全部元素按次序依次存儲。
鏈表的插入刪除元素相對數組較爲簡單,不須要移動元素,且較爲容易實現長度擴充,可是尋找某個元素較爲困難。
數組尋找某個元素較爲簡單,但插入與刪除比較複雜,因爲最大長度須要再編程一開始時指定,故當達到最大長度時,擴充長度不如鏈表方便。
數組和鏈表一些操做的時間複雜度對比:
數組:
鏈表:
3.生活中的案例:
火車,是由一些列車箱鏈接起來;
尋寶遊戲,每一個線索都是下一個線索地點的指針。
append(element)
:向列表尾部添加一個新的元素。insert(position, element)
:向列表指定位置插入一個新的元素。remove(element)
:從列表中移除並返回特定元素(如有多個相同元素則取第一次出現的狀況)。indexOf(element)
:返回元素在列表的索引(如有多個相同元素則取第一次出現的狀況),若是列表中沒有該元素則返回 -1
。removeAt(position)
:從列表中,移除並返回特定位置的一項。isEmpty()
:若是列表不含任何元素,返回 true
,不然返回 false
。size()
:返回列表中元素個數,與數組的 length
屬性相似。toString()
:因爲列表項使用 Node
類,須要重寫繼承自 JavaScript 對象默認的 toString()
方法,讓其只輸出元素的值。解析:
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
}
複製代碼