This commit is contained in:
krahets
2025-10-17 05:33:23 +08:00
parent 9278f3c659
commit 68bb9afb16
113 changed files with 35936 additions and 0 deletions

View File

@@ -0,0 +1,850 @@
---
comments: true
---
# 4.1   配列
<u>配列</u>は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、<u>インデックス</u>として知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください。
![配列の定義と格納方法](array.assets/array_definition.png){ class="animation-figure" }
<p align="center"> 図 4-1 &nbsp; 配列の定義と格納方法 </p>
## 4.1.1 &nbsp; 配列の一般的な操作
### 1. &nbsp; 配列の初期化
配列は必要に応じて2つの方法で初期化できます初期値なしまたは指定された初期値付きです。初期値が指定されていない場合、ほとんどのプログラミング言語は配列要素を$0$に設定します:
=== "Python"
```python title="array.py"
# 配列を初期化
arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]
nums: list[int] = [1, 3, 2, 5, 4]
```
=== "C++"
```cpp title="array.cpp"
/* 配列を初期化 */
// スタックに格納
int arr[5];
int nums[5] = { 1, 3, 2, 5, 4 };
// ヒープに格納(手動でのメモリ解放が必要)
int* arr1 = new int[5];
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
```
=== "Java"
```java title="array.java"
/* 配列を初期化 */
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
int[] nums = { 1, 3, 2, 5, 4 };
```
=== "C#"
```csharp title="array.cs"
/* 配列を初期化 */
int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]
int[] nums = [1, 3, 2, 5, 4];
```
=== "Go"
```go title="array.go"
/* 配列を初期化 */
var arr [5]int
// Goでは、長さを指定[5]intすると配列を示し、指定しない[]intとスライスを示します。
// Goの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。
// extend()メソッドの実装の便宜上、ここではSliceを配列として扱います。
nums := []int{1, 3, 2, 5, 4}
```
=== "Swift"
```swift title="array.swift"
/* 配列を初期化 */
let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
let nums = [1, 3, 2, 5, 4]
```
=== "JS"
```javascript title="array.js"
/* 配列を初期化 */
var arr = new Array(5).fill(0);
var nums = [1, 3, 2, 5, 4];
```
=== "TS"
```typescript title="array.ts"
/* 配列を初期化 */
let arr: number[] = new Array(5).fill(0);
let nums: number[] = [1, 3, 2, 5, 4];
```
=== "Dart"
```dart title="array.dart"
/* 配列を初期化 */
List<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]
List<int> nums = [1, 3, 2, 5, 4];
```
=== "Rust"
```rust title="array.rs"
/* 配列を初期化 */
let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
let slice: &[i32] = &[0; 5];
// Rustでは、長さを指定[i32; 5])すると配列を示し、指定しない(&[i32])とスライスを示します。
// Rustの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。
// 一般的にRustでは動的配列としてVectorが使用されます。
// extend()メソッドの実装の便宜上、ここではベクターを配列として扱います。
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
```
=== "C"
```c title="array.c"
/* 配列を初期化 */
int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
int nums[5] = { 1, 3, 2, 5, 4 };
```
=== "Kotlin"
```kotlin title="array.kt"
```
=== "Zig"
```zig title="array.zig"
// 配列を初期化
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
var nums = [_]i32{ 1, 3, 2, 5, 4 };
```
### 2. &nbsp; 要素へのアクセス
配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。
![配列要素のメモリアドレス計算](array.assets/array_memory_location_calculation.png){ class="animation-figure" }
<p align="center"> 図 4-2 &nbsp; 配列要素のメモリアドレス計算 </p>
上の図で観察されるように、配列のインデックスは慣例的に$0$から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常$1$から始まるためですが、アドレス計算公式内では、**インデックスは本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは$0$で、そのインデックスが$0$であることを検証しています。
配列内の要素へのアクセスは非常に効率的で、$O(1)$時間で任意の要素にランダムアクセスできます。
=== "Python"
```python title="array.py"
def random_access(nums: list[int]) -> int:
"""要素へのランダムアクセス"""
# 区間 [0, len(nums)-1] から数値をランダムに選択
random_index = random.randint(0, len(nums) - 1)
# ランダムな要素を取得して返す
random_num = nums[random_index]
return random_num
```
=== "C++"
```cpp title="array.cpp"
/* 要素への乱数アクセス */
int randomAccess(int *nums, int size) {
// [0, size)の範囲で乱数を選択
int randomIndex = rand() % size;
// 乱数要素を取得して返却
int randomNum = nums[randomIndex];
return randomNum;
}
```
=== "Java"
```java title="array.java"
/* 要素へのランダムアクセス */
int randomAccess(int[] nums) {
// 区間 [0, nums.length) からランダムに数を選択
int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);
// ランダム要素を取得して返す
int randomNum = nums[randomIndex];
return randomNum;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{RandomAccess}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{randomAccess}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{randomAccess}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{randomAccess}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{randomAccess}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{randomAccess}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{random_access}
```
=== "C"
```c title="array.c"
[class]{}-[func]{randomAccess}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{randomAccess}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{random_access}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{randomAccess}
```
### 3. &nbsp; 要素の挿入
配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。
![配列要素挿入の例](array.assets/array_insert_element.png){ class="animation-figure" }
<p align="center"> 図 4-3 &nbsp; 配列要素挿入の例 </p>
配列の長さが固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題を解決する方法は「リスト」の章で探求されます。
=== "Python"
```python title="array.py"
def insert(nums: list[int], num: int, index: int):
"""インデックス index に要素 num を挿入"""
# インデックス index より後のすべての要素を1つ後ろに移動
for i in range(len(nums) - 1, index, -1):
nums[i] = nums[i - 1]
# num を index の位置の要素に代入
nums[index] = num
```
=== "C++"
```cpp title="array.cpp"
/* `index`に要素numを挿入 */
void insert(int *nums, int size, int num, int index) {
// `index`より後のすべての要素を1つ後ろに移動
for (int i = size - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// indexの位置にnumを代入
nums[index] = num;
}
```
=== "Java"
```java title="array.java"
/* `index` に要素 num を挿入 */
void insert(int[] nums, int num, int index) {
// `index` より後のすべての要素を1つ後ろに移動
for (int i = nums.length - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// index の要素に num を代入
nums[index] = num;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Insert}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{insert}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{insert}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{insert}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{insert}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{insert}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{insert}
```
=== "C"
```c title="array.c"
[class]{}-[func]{insert}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{insert}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{insert}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{insert}
```
### 4. &nbsp; 要素の削除
同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。
![配列要素削除の例](array.assets/array_remove_element.png){ class="animation-figure" }
<p align="center"> 図 4-4 &nbsp; 配列要素削除の例 </p>
削除後、元の最後の要素は「意味がない」ものになるため、特定の修正は必要ないことに注意してください。
=== "Python"
```python title="array.py"
def remove(nums: list[int], index: int):
"""インデックス index の要素を削除"""
# インデックス index より後のすべての要素を1つ前に移動
for i in range(index, len(nums) - 1):
nums[i] = nums[i + 1]
```
=== "C++"
```cpp title="array.cpp"
/* `index`の要素を削除 */
void remove(int *nums, int size, int index) {
// `index`より後のすべての要素を1つ前に移動
for (int i = index; i < size - 1; i++) {
nums[i] = nums[i + 1];
}
}
```
=== "Java"
```java title="array.java"
/* `index` の要素を削除 */
void remove(int[] nums, int index) {
// `index` より後のすべての要素を1つ前に移動
for (int i = index; i < nums.length - 1; i++) {
nums[i] = nums[i + 1];
}
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Remove}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{remove}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{remove}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{remove}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{remove}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{remove}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{remove}
```
=== "C"
```c title="array.c"
[class]{}-[func]{removeItem}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{remove}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{remove}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{remove}
```
要約すると、配列の挿入と削除操作には以下の欠点があります:
- **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$で、ここで$n$は配列の長さです。
- **要素の損失**:配列の長さが固定されているため、挿入時に配列の容量を超える要素は失われます。
- **メモリの無駄**:より長い配列を初期化して前部分のみを利用すると、挿入時に「意味のない」末尾要素が生じ、メモリ空間の無駄につながります。
### 5. &nbsp; 配列の走査
ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます:
=== "Python"
```python title="array.py"
def traverse(nums: list[int]):
"""配列の走査"""
count = 0
# インデックスによる配列の走査
for i in range(len(nums)):
count += nums[i]
# 配列要素の走査
for num in nums:
count += num
# データのインデックスと要素の両方を走査
for i, num in enumerate(nums):
count += nums[i]
count += num
```
=== "C++"
```cpp title="array.cpp"
/* 配列の走査 */
void traverse(int *nums, int size) {
int count = 0;
// インデックスによる配列の走査
for (int i = 0; i < size; i++) {
count += nums[i];
}
}
```
=== "Java"
```java title="array.java"
/* 配列を走査 */
void traverse(int[] nums) {
int count = 0;
// インデックスによる配列の走査
for (int i = 0; i < nums.length; i++) {
count += nums[i];
}
// 配列要素の走査
for (int num : nums) {
count += num;
}
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Traverse}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{traverse}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{traverse}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{traverse}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{traverse}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{traverse}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{traverse}
```
=== "C"
```c title="array.c"
[class]{}-[func]{traverse}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{traverse}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{traverse}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{traverse}
```
### 6. &nbsp; 要素の検索
配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。
配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。
=== "Python"
```python title="array.py"
def find(nums: list[int], target: int) -> int:
"""配列内の指定された要素を検索"""
for i in range(len(nums)):
if nums[i] == target:
return i
return -1
```
=== "C++"
```cpp title="array.cpp"
/* 配列内の指定要素を検索 */
int find(int *nums, int size, int target) {
for (int i = 0; i < size; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
```
=== "Java"
```java title="array.java"
/* 配列内で指定された要素を検索 */
int find(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Find}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{find}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{find}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{find}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{find}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{find}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{find}
```
=== "C"
```c title="array.c"
[class]{}-[func]{find}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{find}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{find}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{find}
```
### 7. &nbsp; 配列の拡張
複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。
配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は$O(n)$で、大きな配列では時間がかかる可能性があります。コードは以下の通りです:
=== "Python"
```python title="array.py"
def extend(nums: list[int], enlarge: int) -> list[int]:
"""配列の長さを拡張"""
# 拡張された長さの配列を初期化
res = [0] * (len(nums) + enlarge)
# 元の配列のすべての要素を新しい配列にコピー
for i in range(len(nums)):
res[i] = nums[i]
# 拡張後の新しい配列を返す
return res
```
=== "C++"
```cpp title="array.cpp"
/* 配列長の拡張 */
int *extend(int *nums, int size, int enlarge) {
// 拡張された長さの配列を初期化
int *res = new int[size + enlarge];
// 元の配列の全要素を新しい配列にコピー
for (int i = 0; i < size; i++) {
res[i] = nums[i];
}
// メモリを解放
delete[] nums;
// 拡張後の新しい配列を返却
return res;
}
```
=== "Java"
```java title="array.java"
/* 配列長の拡張 */
int[] extend(int[] nums, int enlarge) {
// 拡張された長さの配列を初期化
int[] res = new int[nums.length + enlarge];
// 元の配列のすべての要素を新しい配列にコピー
for (int i = 0; i < nums.length; i++) {
res[i] = nums[i];
}
// 拡張後の新しい配列を返す
return res;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Extend}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{extend}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{extend}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{extend}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{extend}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{extend}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{extend}
```
=== "C"
```c title="array.c"
[class]{}-[func]{extend}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{extend}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{extend}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{extend}
```
## 4.1.2 &nbsp; 配列の利点と制限
配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。
- **高い空間効率**:配列はデータのための連続したメモリブロックを割り当て、追加の構造的オーバーヘッドの必要性を排除します。
- **ランダムアクセスのサポート**:配列は任意の要素への$O(1)$時間アクセスを可能にします。
- **キャッシュ局所性**:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく、周囲のデータもキャッシュし、高速キャッシュを利用して後続の操作速度を向上させます。
しかし、連続空間格納は諸刃の剣で、以下の制限があります:
- **挿入と削除の効率が低い**:配列に多くの要素が蓄積されると、要素の挿入や削除には大量の要素をシフトする必要があります。
- **固定長**:配列の長さは初期化後に固定されます。配列を拡張するには、すべてのデータを新しい配列にコピーする必要があり、大きなコストがかかります。
- **空間の無駄**:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。
## 4.1.3 &nbsp; 配列の典型的な応用
配列は基本的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑なデータ構造の実装に役立ちます。
- **ランダムアクセス**:配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。
- **ソートと検索**:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。
- **ルックアップテーブル**配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。例えば、文字をASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。
- **機械学習**:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。
- **データ構造の実装**:配列は、スタック、キュー、ハッシュ表、ヒープ、グラフなど、様々なデータ構造を実装するための構成要素として機能します。例えば、グラフの隣接行列表現は本質的に二次元配列です。

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/view-list-outline
---
# 第 4 章 &nbsp; 配列と連結リスト
![配列と連結リスト](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" }
!!! abstract
データ構造の世界は頑丈なレンガの壁に似ています。
配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。
## 章の内容
- [4.1 &nbsp; 配列](array.md)
- [4.2 &nbsp; 連結リスト](linked_list.md)
- [4.3 &nbsp; リスト](list.md)
- [4.4 &nbsp; メモリとキャッシュ *](ram_and_cache.md)
- [4.5 &nbsp; まとめ](summary.md)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
---
comments: true
---
# 4.4 &nbsp; メモリとキャッシュ *
この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました。
実際、**物理構造はプログラムがメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。
## 4.4.1 &nbsp; コンピュータ記憶装置
コンピュータには3種類の記憶装置があります<u>ハードディスク</u>、<u>ランダムアクセスメモリRAM</u>、および<u>キャッシュメモリ</u>です。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。
<p align="center"> 表 4-2 &nbsp; コンピュータ記憶装置 </p>
<div class="center-table" markdown>
| | ハードディスク | メモリ | キャッシュ |
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| 用途 | OS、プログラム、ファイルなどのデータの長期保存 | 現在実行中のプログラムと処理中のデータの一時保存 | 頻繁にアクセスされるデータと命令を保存し、CPUのメモリへのアクセス数を削減 |
| 揮発性 | 電源オフ後もデータは失われない | 電源オフ後にデータは失われる | 電源オフ後にデータは失われる |
| 容量 | より大きい、TBレベル | より小さい、GBレベル | 非常に小さい、MBレベル |
| 速度 | より遅い、数百から数千MB/s | より高速、数十GB/s | 非常に高速、数十から数百GB/s |
| 価格USD | より安価、数セント/GB | より高価、数ドル/GB | 非常に高価、CPUと一緒に価格設定 |
</div>
コンピュータ記憶システムは、下図に示すようにピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量が小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者とエンジニアによる慎重な検討の結果です。
- **ハードディスクをメモリに置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。
- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズが大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
<p align="center"> 図 4-9 &nbsp; コンピュータ記憶システム </p>
!!! tip
コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。
全体的に、**ハードディスクは大量のデータの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータと命令を保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。
下図に示すように、プログラム実行中、データはハードディスクからメモリに読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**し、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。
![ハードディスク、メモリ、キャッシュ間のデータフロー](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
<p align="center"> 図 4-10 &nbsp; ハードディスク、メモリ、キャッシュ間のデータフロー </p>
## 4.4.2 &nbsp; データ構造のメモリ効率
メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制限があります。
一方で、**メモリは限られており、複数のプログラム間で共有できない**ため、データ構造での空間使用の最適化は重要です。配列は要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストは各ノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。
一方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率を低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リストは要素を非連続の場所に保存し、頻繁な挿入と削除はメモリ断片化を悪化させる可能性があります。
## 4.4.3 &nbsp; データ構造のキャッシュ効率
キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、<u>キャッシュミス</u>が発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります。
明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率が高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合は<u>キャッシュヒット率</u>と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。
より高い効率を達成するために、キャッシュは以下のデータロードメカニズムを採用します。
- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。
- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。
- **空間的局所性**:特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するために、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。
- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用して、最近アクセスされたデータを保持してヒット率を向上させます。
実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。
- **占有空間**:連結リスト要素は配列要素よりも多くの空間を占有するため、キャッシュに保持される有効データが少なくなります。
- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。
- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。
- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くのデータがまもなくアクセスされる可能性が高くなります。
全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。
**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。例えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。
- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。
- データ量が非常に大きく、高度に動的で、スタックの予想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リストは大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。

View File

@@ -0,0 +1,85 @@
---
comments: true
---
# 4.5 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います。
- 配列はランダムアクセスをサポートし、使用するメモリが少ない一方で、要素の挿入と削除は非効率的で、初期化後の長さが固定されています。
- 連結リストは参照(ポインタ)の変更によって効率的なノードの挿入と削除を実装し、長さを柔軟に調整できますが、ノードアクセス効率が低く、より多くのメモリを消費します。
- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります。
- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします。
- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります。
- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です。
- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます。
- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。
### 2. &nbsp; Q & A
**Q**:配列をスタックに格納するかヒープに格納するかは、時間と空間効率に影響しますか?
スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本質的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます。
1. 割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります。
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般的に利用可能なメモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています。
3. 柔軟性:スタック上の配列のサイズはコンパイル時に決定される必要がありますが、ヒープ上の配列のサイズは実行時に動的に決定できます。
**Q**:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか?
連結リストは参照ポインタによって接続されたードで構成され、各ードはint、double、string、objectなど、異なる型のデータを格納できます。
対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません。
```shell
# 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス
```
**Q**:ノードを削除した後、`P.next``None`に設定する必要がありますか?
`P.next`を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で`P`に遭遇することはもうありません。これは、ノード`P`がリストから効果的に削除されたことを意味し、`P`が指す場所はもはやリストに影響しません。
ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ード`P`が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、`P.next`の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります。
**Q**:連結リストでは、挿入と削除操作の時間計算量は`O(1)`です。しかし、挿入や削除前の要素検索には`O(n)`時間がかかるので、なぜ時間計算量は`O(n)`ではないのですか?
要素を最初に検索してから削除する場合、時間計算量は確かに`O(n)`です。しかし、連結リストの挿入と削除における`O(1)`の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持し、各挿入と削除操作を`O(1)`にします。
**Q**:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか?
図は単なる定性的な表現であり、定量的分析は特定の状況に依存します。
- 異なる型のード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです。
- ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトです。
**Q**:リストの末尾への要素追加は常に`O(1)`ですか?
要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は`O(n)`になります。
**Q**:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります」という文は、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか?
ここでの空間の無駄は主に2つの側面を指します一方で、リストは初期長で設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常$\times 1.5$などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常は完全に埋めることができません。
**Q**Pythonで`n = [1, 2, 3]`を初期化した後、これら3つの要素のアドレスは連続していますが、`m = [2, 1, 3]`を初期化すると、各要素の`id`は連続していないが`n`のものと同一です。これらの要素のアドレスが連続していない場合、`m`はまだ配列ですか?
リスト要素を連結リストノード`n = [n1, n2, n3, n4, n5]`に置き換える場合、これら5つのードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、`O(1)`時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです。
多くの言語とは異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同じ`id`を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります。
**Q**C++ STLの`std::list`はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制限がありますか?
一方で、アルゴリズムを実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります。
- 空間オーバーヘッド各要素に2つの追加ポインタ前の要素用と次の要素用が必要なため、`std::list`は通常`std::vector`よりも多くの空間を占有します。
- キャッシュ非友好的:データが連続して格納されていないため、`std::list`はキャッシュ利用率が低くなります。一般的に、`std::vector`の方がパフォーマンスが優れています。
他方で、連結リストは主に二分木とグラフに必要です。スタックとキューは、連結リストではなく、プログラミング言語の`stack``queue`クラスを使用して実装されることが多いです。
**Q**:リスト`res = [0] * self.size()`を初期化すると、`res`の各要素は同じアドレスを参照しますか?
いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト`res = [[0]] * self.size()`を初期化すると、同じリスト`[0]`を複数回参照することになります。
**Q**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?
データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。