3.5 小結¶
1. 重點回顧¶
- 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
- 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
- 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
- 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
- 計算機中的基本資料型別包括整數
byte
、short
、int
、long
,浮點數float
、double
,字元char
和布林bool
。它們的取值範圍取決於佔用空間大小和表示方式。 - 原碼、一補數和二補數是在計算機中編碼數字的三種方法,它們之間可以相互轉換。整數的原碼的最高位是符號位,其餘位是數字的值。
- 整數在計算機中是以二補數的形式儲存的。在二補數表示下,計算機可以對正數和負數的加法一視同仁,不需要為減法操作單獨設計特殊的硬體電路,並且不存在正負零歧義的問題。
- 浮點數的編碼由 1 位符號位、8 位指數位和 23 位分數位構成。由於存在指數位,因此浮點數的取值範圍遠大於整數,代價是犧牲了精度。
- ASCII 碼是最早出現的英文字符集,長度為 1 位元組,共收錄 127 個字元。GBK 字符集是常用的中文字符集,共收錄兩萬多個漢字。Unicode 致力於提供一個完整的字符集標準,收錄世界上各種語言的字元,從而解決由於字元編碼方法不一致而導致的亂碼問題。
- UTF-8 是最受歡迎的 Unicode 編碼方法,通用性非常好。它是一種變長的編碼方法,具有很好的擴展性,有效提升了儲存空間的使用效率。UTF-16 和 UTF-32 是等長的編碼方法。在編碼中文時,UTF-16 佔用的空間比 UTF-8 更小。Java 和 C# 等程式語言預設使用 UTF-16 編碼。
2. Q & A¶
Q:為什麼雜湊表同時包含線性資料結構和非線性資料結構?
雜湊表底層是陣列,而為了解決雜湊衝突,我們可能會使用“鏈式位址”(後續“雜湊衝突”章節會講):陣列中每個桶指向一個鏈結串列,當鏈結串列長度超過一定閾值時,又可能被轉化為樹(通常為紅黑樹)。
從儲存的角度來看,雜湊表的底層是陣列,其中每一個桶槽位可能包含一個值,也可能包含一個鏈結串列或一棵樹。因此,雜湊表可能同時包含線性資料結構(陣列、鏈結串列)和非線性資料結構(樹)。
Q:char
型別的長度是 1 位元組嗎?
char
型別的長度由程式語言採用的編碼方法決定。例如,Java、JavaScript、TypeScript、C# 都採用 UTF-16 編碼(儲存 Unicode 碼點),因此 char
型別的長度為 2 位元組。
Q:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。
堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。
Q:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?
在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 ArrayList
的初始容量通常為 10。另外,擴容操作也是自動實現的。詳見後續的“串列”章節。
Q:原碼轉二補數的方法是“先取反後加 1”,那麼二補數轉原碼應該是逆運算“先減 1 後取反”,而二補數轉原碼也一樣可以透過“先取反後加 1”得到,這是為什麼呢?
A:這是因為原碼和二補數的相互轉換實際上是計算“補數”的過程。我們先給出補數的定義:假設 \(a + b = c\) ,那麼我們稱 \(a\) 是 \(b\) 到 \(c\) 的補數,反之也稱 \(b\) 是 \(a\) 到 \(c\) 的補數。
給定一個 \(n = 4\) 位長度的二進位制數 \(0010\) ,如果將這個數字看作原碼(不考慮符號位),那麼它的二補數需透過“先取反後加 1”得到:
我們會發現,原碼和二補數的和是 \(0010 + 1110 = 10000\) ,也就是說,二補數 \(1110\) 是原碼 \(0010\) 到 \(10000\) 的“補數”。這意味著上述“先取反後加 1”實際上是計算到 \(10000\) 的補數的過程。
那麼,二補數 \(1110\) 到 \(10000\) 的“補數”是多少呢?我們依然可以用“先取反後加 1”得到它:
換句話說,原碼和二補數互為對方到 \(10000\) 的“補數”,因此“原碼轉二補數”和“二補數轉原碼”可以用相同的操作(先取反後加 1 )實現。
當然,我們也可以用逆運算來求二補數 \(1110\) 的原碼,即“先減 1 後取反”:
總結來看,“先取反後加 1”和“先減 1 後取反”這兩種運算都是在計算到 \(10000\) 的補數,它們是等價的。
本質上看,“取反”操作實際上是求到 \(1111\) 的補數(因為恆有 原碼 + 一補數 = 1111
);而在一補數基礎上再加 1 得到的二補數,就是到 \(10000\) 的補數。
上述 \(n = 4\) 為例,其可推廣至任意位數的二進位制數。