跳轉至

3.5   小結

1.   重點回顧

  • 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
  • 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
  • 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
  • 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
  • 計算機中的基本資料型別包括整數 byteshortintlong ,浮點數 floatdouble ,字元 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:為什麼雜湊表同時包含線性資料結構和非線性資料結構?

雜湊表底層是陣列,而為了解決雜湊衝突,我們可能會使用“鏈式位址”(後續“雜湊衝突”章節會講):陣列中每個桶指向一個鏈結串列,當鏈結串列長度超過一定閾值時,又可能被轉化為樹(通常為紅黑樹)。

從儲存的角度來看,雜湊表的底層是陣列,其中每一個桶槽位可能包含一個值,也可能包含一個鏈結串列或一棵樹。因此,雜湊表可能同時包含線性資料結構(陣列、鏈結串列)和非線性資料結構(樹)。

Qchar 型別的長度是 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 \rightarrow 1101 \rightarrow 1110 \]

我們會發現,原碼和二補數的和是 \(0010 + 1110 = 10000\) ,也就是說,二補數 \(1110\) 是原碼 \(0010\)\(10000\) 的“補數”。這意味著上述“先取反後加 1”實際上是計算到 \(10000\) 的補數的過程

那麼,二補數 \(1110\)\(10000\) 的“補數”是多少呢?我們依然可以用“先取反後加 1”得到它:

\[ 1110 \rightarrow 0001 \rightarrow 0010 \]

換句話說,原碼和二補數互為對方到 \(10000\) 的“補數”,因此“原碼轉二補數”和“二補數轉原碼”可以用相同的操作(先取反後加 1 )實現。

當然,我們也可以用逆運算來求二補數 \(1110\) 的原碼,即“先減 1 後取反”:

\[ 1110 \rightarrow 1101 \rightarrow 0010 \]

總結來看,“先取反後加 1”和“先減 1 後取反”這兩種運算都是在計算到 \(10000\) 的補數,它們是等價的。

本質上看,“取反”操作實際上是求到 \(1111\) 的補數(因為恆有 原碼 + 一補數 = 1111);而在一補數基礎上再加 1 得到的二補數,就是到 \(10000\) 的補數。

上述 \(n = 4\) 為例,其可推廣至任意位數的二進位制數。