极客时间 算法训练营 第一周总结
學習總結
學習內容
課程內容
- 第三課
- 數組
- 鏈表
- 跳表
- 第四課
- 棧
- 隊列
知識點總結
數組
數組用一塊連續的內存空間,來存儲相同類型的一組數據。
支持隨機訪問,時間復雜度 O(1)
插入、刪除操作比較低效,為了滿足連續空間需要進行數據的搬移,平均情況時間復雜度為O(n)
鏈表
鏈表內存空間可以不連續
鏈表類型有:單鏈表、雙向鏈表、循環鏈表、雙向循環鏈表等
更適合插入、刪除操作頻繁的場景,時間復雜度 O(1)
但是訪問時需要遍歷鏈表 ,平均情況時間復雜度為O(n)
某些情況下雙向鏈表的訪問比單鏈表更高效,如指定訪問某個節點前面的節點
為了提高訪問效率,用空間換時間的設計思路出現跳表
跳表
通過空間換時間,構建多級索引來提高查詢的效率,實現了基于鏈表的“二分查找”
是一種動態數據結構,支持快速的插入、刪除、查找操作,時間復雜度為O(nlogn)
棧
棧是一種受限的數據結構,只支持入棧和出棧操作,其入棧、出棧時間復雜度為O(1)。特點是先入后出。
隊列
特點是先入先出,常見 有 優先隊列,雙端隊列
關于迭代和遞歸的一些總結
個人理解如果不同意見歡迎探討,如果錯誤歡迎指正
迭代和遞歸都是要找到其重復處理單元,不同的地方是
迭代:思路是從前向后,大多數情況下使用哨兵節點能簡化處理流程
遞歸:思路是從后向前,僅關注當前層和下一層,重復操作就不要向下考慮太多不然容易暈
遞歸
先找重復處理單元,再找遞歸終止條件,解決了這兩個問題就很好處理了
就鏈表處理,我將遞歸相關題目歸納為以下幾點:
- 先找重復處理單元
- 遞歸終止條件
- 遞歸前處理(遞歸到下一層前處理):
- 遞歸(遞歸到下一層):
- 遞歸后處理(下一層遞歸返回后處理)
具體可以參看下面的例子
迭代
同樣先找重復處理單元,再找迭代終止條件
就鏈表處理,我將迭代相關題目歸納為以下幾點:
- 先找重復處理單元
- 迭代終止條件
- 迭代前處理
- 迭代處理
- 迭代后處理
具體可以參看下面的例子
例子
例子 24. 兩兩交換鏈表中的節點
遞歸實現
#@author:leacoder #@des: 遞歸實現 兩兩交換鏈表中的節點'''重復處理單元: 兩兩交換 鏈表可以分為 待處理 + 當前層正處理(兩兩交換的兩個節點) + 下層遞歸已處理下一層遞歸返回已處理頭結點 當前層就知道: 需要交換的 a、b 兩節點 以及 后續已處理頭節點遞歸終止條件: head.next 為 None 可以將 head 為 None 的特殊情況一起做判斷 if not head or not head.next:return head遞歸前處理(遞歸到下一層前處理): 這里兩兩交換鏈表中的節點 , 下一次遞歸從 下 兩個的結點開始 題 25. K 個一組翻轉鏈表 則另一種處理 可以記錄下 當前的 next next = head.next遞歸(遞歸到下一層): 下兩位,使用遞歸前處理的 next p = swapPairs(next.next) 題 25. K 個一組翻轉鏈表 則 會有不同 遞歸后處理(下層遞歸返回后處理):1->2->3->4 2->1->4->3''' class Solution:def swapPairs(self, head: ListNode) -> ListNode:# 遞歸終止條件:if not head or not head.next:return head# 遞歸前處理(遞歸到下一層前處理):# 當前層 需交換節點 為 head (a) 、head.next (b)next = head.next# 遞歸(遞歸到下一層):resultmp = self.swapPairs(next.next) # 下移兩位 返回值為兩兩交換后 兩個節點中的前一個節點# 遞歸后處理(下層遞歸返回后處理):head.next = resultmp # 兩兩交換next.next = head # 兩兩交換''' 語法糖head.next, next.next = self.swapPairs(next.next), head'''return next # 返回 兩兩交換后 兩個節點中的前一個節點迭代實現
#@author:leacoder #@des: 迭代實現 兩兩交換鏈表中的節點''' 利用哨兵簡化操作1->2->3->42->1->4->3重復處理單元: 兩兩交換 鏈表可以分為 已處理 + 正處理(兩兩交換的兩個節點) + 后續迭代待處理迭代終止條件: 兩兩交換,迭代處理這兩個節點,那么后續節點不足兩個時跳出迭代 prev.next ,prev.next.next 為 None 迭代前處理: 利用哨兵簡化操作,添加哨兵節點 prev 最終結果 以 2->1->4->3 為例 其頭節點為 2 由于兩兩交換 prev 也會跟隨下移,先 用 retult = prev 記錄prev下移前的值用于最終結果返回迭代處理:1->2->3->4 為例 將需要翻轉操作的兩個節點記錄(要斷開連接,先記錄) a = prev.next (1) b = prev.next.next (2) 交換兩節點 prev 節點下移,迭代開始新一輪處理 (由于兩兩交換,跳過兩個節點)迭代后處理: 無特殊處理'''class Solution:def swapPairs(self, head: ListNode) -> ListNode:# 迭代前處理:prev = ListNode(None) # 哨兵prev.next = headretult = prev # 記錄 第一次被操作前的prev 用于返回結果# 迭代終止條件 prev.next ,prev.next.next 為 None while prev.next and prev.next.next: # 迭代處理:# 以 1 2 反轉為例,prev為哨兵a = prev.next # 由于 要操作 prev.next (1) 和 prev.next.next (2) 先記錄下來b = prev.next.next # 記錄 要交換的兩節點prev.next = b # (2)a.next = b.next # (3)b.next = a # (1)prev = a # (1) # 向下移動2位''' 使用語法糖prev.next,a.next,b.next= b,b.next,a # 2個節點交換,注意哨兵的next改變了prev = a # 向下移動2位'''# 迭代后處理 無return retult.next例子 25. K 個一組翻轉鏈表
遞歸實現
#@author:leacoder #@des: 遞歸實現 k個一組翻轉鏈表'''重復處理單元: k個一組翻轉 鏈表可以分為 : 待處理 + 當前層正處理(k個一組鏈表) + 下層遞歸已處理下一層遞歸返回已處理頭結點 當前層就知道: 需要翻轉k個一組鏈表的鏈表頭、鏈表尾 鏈表頭 head 參數 鏈表尾 遍歷 k 個節點遞歸終止條件: 后續迭代待處理剩余節點不足 k 個遞歸前處理(遞歸到下一層前處理): k個一組翻轉鏈表,下一次遞歸從 下 k 個節點開始 記錄 翻轉前的 鏈表頭 鏈表尾遞歸(遞歸到下一層): 下k位,使用遞歸前處理的 鏈表尾.next遞歸后處理(下層遞歸返回后處理): 將下一層遞歸返回結果加入到原鏈表 K 個一組鏈表翻轉,''' class Solution:def reverseKGroup(self, head: ListNode, k: int) -> ListNode:# 用于記錄 翻轉前的 鏈表頭 鏈表尾end = headcount = 0# 遞歸終止條件while end and count!= k:end = end.nextcount += 1if count == k: # 遞歸終止條件# 遞歸前處理(遞歸到下一層前處理) # 記錄 翻轉前的 鏈表頭 head 鏈表尾 end 在前面已做# 遞歸(遞歸到下一層):resultmp = self.reverseKGroup(end,k) # 下層 已處理頭結點 # 遞歸后處理(下層遞歸返回后處理)while count: # K 個一組鏈表翻轉,tmp = head.nexthead.next = resultmp # 將下一層遞歸返回結果加入到原鏈表resultmp = headhead = tmpcount -= 1head = resultmp # 翻轉后鏈表頭return head迭代實現
#@author:leacoder #@des: 迭代實現 k個一組翻轉鏈表''' 利用哨兵簡化操作 prev = ListNode(None) # 哨兵 prev.next = head prev 每次迭代下移(下移位數為k)重復處理單元: k個一組翻轉 鏈表可以分為 :已處理 + 正處理(k個一組鏈表) + 后續迭代待處理 正處理(k個一組鏈表)需要知道 鏈表頭和鏈表尾 鏈表頭 可以通過 prev 的 next 獲取 鏈表尾 需要遍歷k個節點獲取迭代終止條件: 后續迭代待處理剩余節點不足 k 個迭代前處理: result 記錄 prev 下移前的值 用于 結果返回 end 參數 記錄尾節點 prev、end 每次迭代指向同一節點迭代處理: 遍歷k個節點獲取 鏈表尾 構建 k個一組鏈表 并進行翻轉 reverse() reverse() 翻轉函數返回 翻轉后新鏈表頭 將翻轉后新鏈接入原鏈表 prev 下移 end 下移迭代后處理: 無'''class Solution:def reverseKGroup(self, head: ListNode, k: int) -> ListNode:# 迭代前處理:prev = ListNode(None) # 哨兵prev.next = headresult = prevend = prev # end 記錄 k個一組鏈表 鏈表尾 prev.next 記錄 k個一組鏈表 鏈表頭# 迭代終止條件:while(end.next): # 剩余節點不足 k 個for i in range(k): # 遍歷k個節點獲取 鏈表尾 并 判斷 剩余節點 是否不足 k 個if not end: # 不足 k 個breakend = end.nextif not end: # 不足 k 個break'''start = prev.next # k個一組鏈表 鏈表頭next = end.next # 記錄 原 end.next end.next = None # k個一組鏈表 鏈表尾'''# 構建 k個一組的鏈表start, next, end.next = prev.next, end.next, None# 翻轉 k個一組的鏈表prev.next = self.reverse(start) # 返回 翻轉后新鏈表頭 接入原鏈表start.next = next # 接入原鏈表 start 為 翻轉后新鏈表尾prev = startend = startreturn result.nextdef reverse(self, head: ListNode):prev = Nonecurr = headwhile(curr):'''tmp = curr.nextcurr.next = prevprev = currcurr = tmp'''curr.next, prev, curr = prev, curr, curr.nextreturn prev課后作業 改寫Deque的代碼
課程中示例代碼-Deque
代碼
import java.util.LinkedList; import java.util.Deque;public class DequeExample {public static void main(String[] args) {Deque<String> deque = new LinkedList<String>();deque.push("a");deque.push("b");deque.push("c");System.out.println(deque);String str = deque.peek();System.out.println(str);System.out.println(deque);while (deque.size() > 0) {System.out.println(deque.pop());}System.out.println(deque);} }輸出:
[c, b, a] c [c, b, a] c b a []接口說明
push
public void push?(E e)
Pushes an element onto the stack represented by this list. In other words, inserts the element at the front of this list. This method is equivalent to addFirst(E).將元素壓入此列表表示的堆棧中。換句話說,將元素插入此列表的前面。
此方法等效于addFirst(E)
pop
public E pop()
Pops an element from the stack represented by this list. In other words, removes and returns the first element of this list. This method is equivalent to removeFirst().從此列表表示的堆棧中彈出一個元素。換句話說,刪除并返回此列表的第一個元素。
此方法等效于equivalent to removeFirst().
peek
public E peek()
Retrieves, but does not remove, the head (first element) of this list.檢索但不刪除此列表的頭(第一個元素)。
addFirst 改寫 Deque 代碼
代碼
import java.util.LinkedList; import java.util.Deque;public class DequeExample {public static void main(String[] args) {Deque<String> deque = new LinkedList<String>();deque.addFirst("a"); // 使用 addFirst 替換 pushdeque.addFirst("b");deque.addFirst("c");System.out.println(deque);String str = deque.peek();System.out.println(str);System.out.println(deque);while (deque.size() > 0) {System.out.println(deque.removeFirst()); // 使用 removeFirst 替換 pop}System.out.println(deque);} }輸出:
[c, b, a] c [c, b, a] c b a []接口說明
addFirst
void addFirst?(E e)
Inserts the specified element at the front of this deque if it is possible to do so immediately without violating capacity restrictions, throwing an IllegalStateException if no space is currently available. When using a capacity-restricted deque, it is generally preferable to use method offerFirst(E).如果可以在不違反容量限制的情況下立即執行此操作,則將指定的元素插入此雙端隊列的前面,如果當前沒有可用空間,則拋出IllegalStateException。使用容量受限的雙端隊列時,通常最好使用方法offerFirst(E)。
removeFirst
E removeFirst()
Retrieves and removes the first element of this deque. This method differs from pollFirst only in that it throws an exception if this deque is empty.檢索并刪除此雙端隊列的第一個元素。此方法與pollFirst的不同之處僅在于,如果此雙端隊列為空,則它將引發異常。
Queue和PriorityQueu源碼分析
什么是 Queue
數據結構中的隊列,先進先出式的數據結構,所有新元素都插入隊列的末尾,移除元素都移除隊列的頭部。主要注意的時,Java中的Queue是一個接口。
Queue 源碼分析
public interface Queue<E> extends Collection<E> {boolean add(E e); //往隊列插入元素,如果出現異常會拋出異常boolean offer(E e); //往隊列插入元素,如果出現異常則返回falseE remove(); //移除隊列元素,如果出現異常會拋出異常E poll(); //移除隊列元素,如果出現異常則返回nullE element(); //獲取隊列頭部元素,如果出現異常會拋出異常E peek(); //獲取隊列頭部元素,如果出現異常則返回null }上面六個函數總體上分為兩類:安全的會進行容量檢查的(add,remove,element),如果隊列沒有值,則取元素會拋出IlleaglStatementException異常。不安全的不進行容量控制的(offer,poll,peek )。
AbstractQueue 是Queue 的抽象實現類, AbstractQueue 也繼承自 AbstractCollection 。AbstractQueue 實現的方法不多,主要就 add、remove、element 三個方法的操作失敗拋出了異常。
什么是 PriorityQueue
PriorityQueue 優先級隊列, ,不同于普通的遵循FIFO(先進先出)規則的隊列,每次都選出優先級最高的元素出隊,優先隊列里實際是維護了這樣的一個堆,通過堆使得每次取出的元素總是最小的(用戶可以自定義比較方法,相當于用戶設定優先級)。 PriorityQueue 是一個小頂堆,是非線程安全的;PriorityQueue不是有序的,只有堆頂存儲著最小的元素;從數據的存儲結構看, PriorityQueue 一個數組;從邏輯結構看, PriorityQueue 是一棵平衡二叉樹。
什么是 PriorityQueue
PriorityQueue 優先級隊列, ,不同于普通的遵循FIFO(先進先出)規則的隊列,每次都選出優先級最高的元素出隊,優先隊列里實際是維護了這樣的一個堆,通過堆使得每次取出的元素總是最小的(用戶可以自定義比較方法,相當于用戶設定優先級)。 PriorityQueue 是一個小頂堆,是非線程安全的;PriorityQueue不是有序的,只有堆頂存儲著最小的元素;從數據的存儲結構看, PriorityQueue 一個數組;從邏輯結構看, PriorityQueue 是一棵平衡二叉樹。
PriorityQueue 源碼分析
繼承關系
public class PriorityQueue<E> extends AbstractQueue<E>implements java.io.Serializable{// 略}PriorityQueue 繼承了AbstractQueue 類,而AbstractQueue 類實現了Queue接口。
主要屬性
// 默認容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; // 存儲元素的數組,Object類型的 transient Object[] queue; // 元素個數 int size; // 比較器 private final Comparator<? super E> comparator; // 修改次數 transient int modCount;transient Object[] queue
源碼注釋
優先級隊列是一個平衡的二叉樹堆,queue[n]的兩個子節點queue[2n+1] and queue[2(n+1)]。 優先級隊列 按照 comparator 進行排序或者 按自然順序排序。 如果 comparator 為null ,堆中每一個節點 n 以及 n的后代d, n<=d。假設隊列為非空,則具有最小值的元素位于queue [0]中。
常用構造函數
- 1、創建一個優先隊列對象,默認大小,隊列中的元素按照自然順序排序。
- 2、創建一個指定大小的優先隊列對象,隊列中的元素按照自然順序排序。
- 3、創建一個默認大小(11)的優先隊列對象,隊列中的元素按照指定比較器進行排序。
- 4、根據指定的大小和比較器來創建一個優先隊列對象。
add(e)/offer(e)方法介紹
add方法
插入一個元素到優先隊列中
add方法的源代碼如下:
public boolean add(E e) {return offer(e); }從源碼中可以看出add方法直接調用了offer方法。
offer(e)方法
public boolean offer(E e) {if (e == null) //檢查是否為null, 如果為null 則拋空指針異常throw new NullPointerException();modCount++;int i = size;if (i >= queue.length) // 檢查容量是否足夠, 不足進行擴容grow(i + 1); // 擴容函數siftUp(i, e); // 在數組的末尾插入新的元素,向上調整,使之保持為二叉堆的特性。size = i + 1;return true; }1)首先檢查要添加的元素e是否為null,如果為null,則拋空指針異常,如果不為null,則進行2
2)判斷數組是否已滿,如果已滿,則進行擴容,否則將元素加入到數組末尾即可。
由于這個數組表示的是一個“二叉堆”,因此還需要進行相應的調整操作,使得這個數組在添加元素之后依然保持的是二叉堆的特性。
grow(int minCapacity)方法
//增加數組的容量 private void grow(int minCapacity) {int oldCapacity = queue.length;// Double size if small; else grow by 50%//如果比較小,則擴容為原來的2倍,否則只擴容為原來的1.5倍int newCapacity = oldCapacity + ((oldCapacity < 64) ?(oldCapacity + 2) :(oldCapacity >> 1));// overflow-conscious codeif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);queue = Arrays.copyOf(queue, newCapacity); }hugeCapacity(int minCapacity) 方法
private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE; }注:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public static final int MAX_VALUE = 0x7fffffff;
當 大于 MAX_ARRAY_SIZE 時擴容記性了特殊處理
siftUp(int k, E x)方法
將元素x插入到queue[k]位置上,并進行調整使之具有二叉堆特性
private void siftUp(int k, E x) {if (comparator != null)siftUpUsingComparator(k, x); // 使用比較器 comparatorelsesiftUpComparable(k, x); }siftUpUsingComparator和siftUpComparable源碼如下
從 位置 k 向數組起始位置 調整使之具有二叉堆特性,自下而上的堆化,
也就是 二叉堆插入元素思想操作:
可參見 拜托,面試別再問我堆(排序)了!
siftUpComparable(int k, E x)方法
@SuppressWarnings("unchecked") private void siftUpComparable(int k, E x) {Comparable<? super E> key = (Comparable<? super E>) x;while (k > 0) {int parent = (k - 1) >>> 1; // 找 parent 父節點Object e = queue[parent];if (key.compareTo((E) e) >= 0) // 比較 按 自然順序 排列break;queue[k] = e; k = parent; }queue[k] = key; }siftUpUsingComparator(int k, E x)方法
@SuppressWarnings("unchecked") private void siftUpUsingComparator(int k, E x) {while (k > 0) {int parent = (k - 1) >>> 1;Object e = queue[parent];if (comparator.compare(x, (E) e) >= 0) // 使用比較器 comparatorbreak;queue[k] = e;k = parent;}queue[k] = x; }poll()方法介紹
取出優先隊列中的第一個元素
public E poll() {if (size == 0) // 優先隊列大小判斷return null;int s = --size;modCount++;E result = (E) queue[0]; // 取出第一個元素E x = (E) queue[s]; // 取出最后一個元素queue[s] = null;if (s != 0)siftDown(0, x); return result; }poll方法:取出queue[0]元素,然后將queue[size-1]插入到queue[0],然后自上而下堆化來保持二叉堆的特性。
siftDown(int k, E x)方法
//將元素x存儲在queue[k],并進行相應的調整 private void siftDown(int k, E x) {if (comparator != null)siftDownUsingComparator(k, x);elsesiftDownComparable(k, x); }siftDownComparable(int k, E x)方法
private void siftDownComparable(int k, E x) {Comparable<? super E> key = (Comparable<? super E>)x;int half = size >>> 1; // loop while a non-leafwhile (k < half) {int child = (k << 1) + 1; // assume left child is leastObject c = queue[child];int right = child + 1;if (right < size &&((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) //兩個兒子節點較小的那一個c = queue[child = right];if (key.compareTo((E) c) <= 0)break;queue[k] = c;k = child;}queue[k] = key; }siftDownUsingComparator(int k, E x)方法
private void siftDownUsingComparator(int k, E x) {int half = size >>> 1;while (k < half) {int child = (k << 1) + 1;Object c = queue[child];int right = child + 1;if (right < size &&comparator.compare((E) c, (E) queue[right]) > 0)c = queue[child = right];if (comparator.compare(x, (E) c) <= 0)break;queue[k] = c;k = child;}queue[k] = x; }remove(Object o) 方法介紹
移除指定元素
public boolean remove(Object o) {int i = indexOf(o); // 判斷位置if (i == -1) //沒有在數組中找到return false;else {removeAt(i); //進行刪除并調整return true;} }indexOf(Object o)方法
private int indexOf(Object o) {if (o != null) {for (int i = 0; i < size; i++) // 數組遍歷過程if (o.equals(queue[i]))return i;}return -1; }找到對象o在數組中出現的第一個索引
emoveAt(int i)方法
E removeAt(int i) {// assert i >= 0 && i < size;modCount++;int s = --size;if (s == i) // removed last elementqueue[i] = null;else {E moved = (E) queue[s]; // 取出最后一個元素queue[s] = null;siftDown(i, moved); //從i向下調整堆if (queue[i] == moved) { // 如果發現調整后 moved 還在 i 位置沒有下沉,向上調整看是否上浮siftUp(i, moved);if (queue[i] != moved)return moved;}}return null; }堆插入元素與刪除堆頂元素操作
以下面這個對為例
用數組來存儲為:
插入元素
往堆中插入一個元素后,需要繼續滿足堆的兩個特性,即:
(1)堆是一顆完全二叉樹;
(2)堆中某個節點的值總是不大于(或不小于)其父節點的值。
把元素插入到最后一層最后一個節點往后一位的位置,也就是在數組的末尾插入新的元素
,插入之后可能不再滿足條件(2),這時候我們需要調整,自下而上的堆化。
上面那個堆插入元素2,我們把它放在9后面,這時不滿足條件(2)了,我們就需要堆化。(這是一個小頂堆)
從插入元素的過程,我們知道每次與 n/(2x)n/(2^x)n/(2x) 的位置進行比較,所以,插入元素的時間復雜度為 O(logn)O(log n)O(logn)。
刪除堆頂元素
刪除了堆頂元素后,要使得還滿足堆的兩個特性。把最后一個元素移到根節點的位置,這時候就滿足條件(1),之后就需要堆化了使它滿足條件(2)
從刪除元素的過程,我們知道把最后一個元素拿到根節點后,每次與 2n2n2n 和 (2n+1)(2n+1)(2n+1) 位置的元素比較,取其小者,所以,刪除元素的時間復雜度也為 O(logn)O(log n)O(logn) 。
回到 remove(Object o) 方法 ,不是刪除堆頂元而是刪除指定元素,源碼中會先找Object o在數組中位置 i,再取出最后一個元素從i自上而下堆化,如果最后一個元素沒有下沉,會從i自下而上堆化
總結
以上是生活随笔為你收集整理的极客时间 算法训练营 第一周总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 批量添加手机联系人:csv/excel转
- 下一篇: linux 开山(尚硅谷)--听课笔记-