array indexing
array indexing
「索引」是電腦的絕技!一個元素存放到陣列之後,不論是在陣列的哪個地方,只要利用索引值(index),就能一瞬間找到元素。大多數的演算法都運用了「索引」的技巧,讓程式執行時間更短。
以下介紹索引的常見運用方式。
一、定位
將物件放入陣列中,array[位置] = 物件。
當元素很多時,我們可以放到陣列裡。我們只要記錄索引值,依舊可以常數時間得到元素。
範例:求最大值
將元素連續地放入陣列,若想記錄一元素,僅需一索引值。
範例:求子字串
將元素連續地放入陣列,若想記錄一區間,僅需頭尾的索引值。
範例:連續數字和
將元素連續地放入陣列,利用問題本身的數學性質以及索引值,快速得到答案。
範例:求中位數
將元素依照大小順序並連續地放入陣列,利用索引值得到位於中間的元素。
範例:二元搜尋法(binary search)
將元素依照大小順序並連續地放入陣列,然後夾擠索引值。
如果元素放入串列,則沒有索引值,無法使用二元搜尋法。
範例:二元樹(binary tree)
元素的索引值對應到樹的結構,是一種特殊的定位。
範例:堆疊(stack)、佇列(queue)
元素連續地放入陣列,然後以改變索引值的方式,來動態增減堆疊及佇列的元素。
二、歸類並標記
物件直接作為陣列的索引值,array[物件] = 物件的屬性。
範例:正整數集合
物件:正整數。物件的屬性:是否在集合裡頭出現。
範例:統計英文字母出現次數
物件:英文字母。物件的屬性:英文字母的出現次數。
範例:計數排序法(counting sort)
索引值的大小順序,恰是元素的大小順序,亦可用於排序。
範例:雜湊表(hash table)
元素的索引值由特殊方法決定,是一種特殊的歸類。
三、轉換
array[物件] = 另一個物件。概念等同數學術語「函數」。
範例:取代(substitution)、移位(transposition)
密碼學的基礎概念。取代是文字的轉換,移位是位置的轉換。
定址的時間複雜度
當索引值大小為N,有人認為定址的時間複雜度是O(log₂N),也有人認為是O(1)。兩種說法都有依據。
數學的觀點:N總共有log₂N個位元,運用二元樹的觀念,依照各個位元的數值是0或是1進行分枝,分枝到底後就完成定址了。所以定址的時間複雜度是O(log₂N)。
電路的觀點:一顆中央處理器可以平行處理32位元(現在已有64位元),只要是介於0到2³²-1的索引值,都可以在1單位時間完成定址,而不必用32單位時間來完成定址。所以定址的時間複雜度是O(1)。
大家傾向假設定址的時間複雜度是O(1)。
定址的範圍
方才提到一顆中央處理器可以平行處理32位元,理論上可以定址到2³²以內的位址。一個位址一般擁有1byte的記憶體大小,所以可以運用的記憶體就有2³²byte = 4GB 這麼多。
但是作業系統會保留一些位址、預留一些記憶體空間以維持系統運作,所以使用者實際可以運用的記憶體其實不到4GB。
當記憶體沒有插到4GB的時候,作業系統利用一種叫做virtual memory的技術,以硬碟空間補足記憶體不足4GB的部份。
位址是連續不斷的,我們寫程式也都直接假設位址對應到的記憶體空間是連續不斷的,然而實際上並不是連續的。作業系統運用一種叫做paging的技術,藉由page table,讓記憶體看起來是連續的。
recursion
recursion
在函式內部呼叫原本的函式,叫做「遞迴」。
遞迴與迴圈一樣,將一件事情重複很多次,每次都改變一點點。遞迴與迴圈一樣,必須設定起始條件、結束條件、改變量,以避免無窮遞迴。
範例:階乘
1乘以2乘以3……乘以n。
範例:兔子數列
0 1 1 2 3 5 8……,求第n項。公式f(n) = f(n-1) + f(n-2)。
迴圈輔以堆疊才能樹狀分枝。遞迴可以輕易地樹狀分枝。
UVa 110 177 183 839
作業系統的記憶體管理
作業系統執行一支程式,首先會將程式指令載入到記憶體空間,另外還會預留固定的記憶體空間以便執行程式,稱作call stack。
其內容包含:一、呼叫函式所傳入的參數。二、執行函式所宣告的變數。三、結束函式所回去的位址(某一行程式指令的位址)。
call stack的行為,宛如資料結構stack。每當開始使用一個函式,就把對應資料放入call stack。每當一個函式結束使用,就從call stack移除對應資料。函式層層呼叫,對應資料層層疊起。
作業系統規定了一支程式的call stack記憶體空間大小。例如Windows是1MB,Linux是8MB(不超過page table一個欄位的空間大小)。超過限制,稱作stack overflow。此時作業系統將立即中止程式,以策安全,稱作crash。
遞迴容易引發stack overflow
每當使用函式,就會使用call stack。
人類設計程式,在函式內部呼叫函式,深度大約十層、二十層。call stack足以承擔。
至於遞迴,不斷地在函式內部呼叫原本的函式,輕輕鬆鬆達到成千上萬層。call stack極易耗盡,引發stack overflow。
call stack的原始用途不包括成千上萬層的遞迴。這是個怪招,平常沒人這樣做,大家當作娛樂看看就好。儘管在C++裡面是怪招,但是在Haskell裡面卻是基本功。
recursion = loop + stack
遞迴的運作原理,其實是迴圈和堆疊。凡是遞迴能做到的,迴圈和堆疊也能做到。原理就是作業系統的call stack。
如果遞迴深度只有幾層,那麼大家習慣以遞迴代替迴圈和堆疊。事情交給作業系統的既有機制,不必額外建立迴圈和堆疊。
如果遞迴深度達到成千上萬層,那麼大家習慣以迴圈和堆疊代替遞迴。如此一來,可以節省許多記憶體空間:呼叫函式所傳入的參數(視 情況可以重複使用)、結束函式所回去的位址(完全不需要)。另外也可以節省一些執行時間。
interval
array indexing
數10個數字,一般而言,從1開始,到10結束。
數10格陣列,有些不同,從0開始,到9結束。
陣列索引值,從0開始。原因可能是指標。存取陣列首格,不需要將記憶體位址+1。
loop
迴圈索引值,從0開始,到n-1結束。原因是陣列。
小技巧:為了減少計算步驟-1,於是<= n-1改成< n。
結束條件是等於,明確合理,「這裡就是終點」。
結束條件是小於,不夠直覺,「似乎還沒到終點」。
為了減少計算步驟-1,只好採用不夠直覺的方式。
metaprogramming
metaprogramming
設計一支程式來製造程式碼,令該程式碼充分運用程式語言自身擁有的能力,輕鬆地、更有效率地完成更多事情。
範例:四則運算式
5 + 8 × (2 - 3) + 7 × -6 / (2 - 1) + 1
身經百戰的演算法設計高手,必然不假思索說出:stack,把所有符號依序放入stack,依照運算符號的優先次序push和pop。聽來簡單,實作起來還是頗麻煩。
這裡要介紹的是更輕鬆、更強悍的方法:寫程式製造一支會進行四則運算的程式。大家都知道C/C++的語法當中,就有四則運算的語法了。現在來設計一支程式,製作出四則運算的程式碼吧!
如果輸入方才的四則運算式,就會產生如下程式碼,檔名為temp.cpp。
然後編譯temp.cpp、執行一下,就有答案了。甚至可以把編譯、執行的指令,統統寫進程式碼當中:
範例:quine
一支程式,其功能是輸出本身程式碼。純娛樂。
template metaprogramming
C有個功能叫macro,可以代換程式碼。C++有個更厲害的功能叫template,可以遞迴代換程式碼。
運用template,編譯時期即可計算答案,令答案變成程式碼的一部分;執行時期不必花時間計算,直接印出答案!
不過這是個怪招,平常沒人這樣做,大家當作娛樂看看就好。儘管在C++裡面是怪招,但是在Haskell裡面卻是基本功。
範例:階乘
task🚧
process management
Java memory model
行程管理問題、行程管理策略:
race condition concurrency control
memory management
想要了解作業系統,也許從記憶體管理開始講起。
作業系統/虛擬機器,記憶體空間劃分為多種區塊,作為不同用途。每種作業系統/虛擬機器的劃分方式大同小異,跟程式有關的記憶體區塊大致相同:heap和stack。
heap事先不知道記憶體用量(動態)。stack事先知道記憶體用量(靜態)。
注意到,作業系統heap和stack、資料結構heap和stack,兩者不是相同事物。
幾個比較有名的作業系統/虛擬機器的記憶體劃分方式:
Linux分為kernel virtual memory、user stack、shared library、runtime heap、……。
user stack大致就是先前提到的call stack,差別在於user stack額外包含使用者編號、程序編號、執行時刻、使用權限、……。不過目前大家傾向將兩者視作相同事物,方便解釋。
https://hemantra.medium.com/linux-memory-management-all-you-need-to-know-d1dbdda8b386
Java的JVM額外劃分出metaspace和code。網路上每個人畫出來的示意圖都不一樣,非常奇葩。
https://techvidvan.com/tutorials/java-virtual-machine/
JavaScript的V8則是把code放在heap。
每種程式語言各有一套記憶體分類方式。有趣的是,登場場合不一致。顯然大家尚未取得共識。有些是語法關鍵字,有些是標準函式庫,有些是編譯器。
幾個比較有名的程式語言的記憶體分類方式:
C的memory layout有四種:text、data、heap、stack。Linux記憶體管理方式正是依循這個分類方式。Java和JavaScript以及其他諸多程式語言也跟進這個分類方式。【尚待確認】
C++的storage duration有四種。有些對應到heap,有些對應到stack。【待補表格】
記憶體管理問題、記憶體管理策略:
memory leak garbage collection memory fragmentation storage allocation