Re-translate the Japanese version (#1871)

* Retranslate Japanese docs with GPT-5.4

* Retranslate Japanese code with GPT-5.4
This commit is contained in:
Yudong Jin
2026-03-30 07:30:15 +08:00
committed by GitHub
parent fe6443235b
commit d7b2277d2b
1444 changed files with 83312 additions and 8363 deletions
+72 -58
View File
@@ -1,19 +1,19 @@
# 配列
<u>配列</u>は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、<u>インデックス</u>として知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください
<u>配列array</u>は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納ます。要素が配列内にある位置を、その要素の<u>インデックス(index</u>と呼びます。下図は、配列の主要な概念と格納方式を示しています
![配列の定義と格納方](array.assets/array_definition.png)
![配列の定義と格納方](array.assets/array_definition.png)
## 配列の一般的な操作
### 配列の初期化
配列は必要に応じて2つの方法で初期化できます:初期値なしまたは指定された初期値付きです。初期値指定されていない場合、ほとんどのプログラミング言語は配列要素$0$に設定します
必要に応じて、配列の初期化方法として初期値なしと初期値ありの 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]
```
@@ -21,11 +21,11 @@
=== "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 };
```
@@ -33,7 +33,7 @@
=== "Java"
```java title="array.java"
/* 配列を初期化 */
/* 配列を初期化する */
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
int[] nums = { 1, 3, 2, 5, 4 };
```
@@ -41,7 +41,7 @@
=== "C#"
```csharp title="array.cs"
/* 配列を初期化 */
/* 配列を初期化する */
int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]
int[] nums = [1, 3, 2, 5, 4];
```
@@ -49,18 +49,18 @@
=== "Go"
```go title="array.go"
/* 配列を初期化 */
/* 配列を初期化する */
var arr [5]int
// Goでは、長さを指定([5]int)すると配列を示し、指定しない([]intスライスを示します。
// Goの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。
// extend()メソッド実装の便宜上、ここではSliceを配列として扱います。
// Go では、長さを指定する場合[5]intは配列であり、長さを指定しない場合[]intスライス
// Go の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない
// 拡張 extend() メソッド実装しやすくするため、以下ではスライス(Sliceを配列Array)として扱う
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]
```
@@ -68,7 +68,7 @@
=== "JS"
```javascript title="array.js"
/* 配列を初期化 */
/* 配列を初期化する */
var arr = new Array(5).fill(0);
var nums = [1, 3, 2, 5, 4];
```
@@ -76,7 +76,7 @@
=== "TS"
```typescript title="array.ts"
/* 配列を初期化 */
/* 配列を初期化する */
let arr: number[] = new Array(5).fill(0);
let nums: number[] = [1, 3, 2, 5, 4];
```
@@ -84,7 +84,7 @@
=== "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];
```
@@ -92,20 +92,20 @@
=== "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()メソッド実装の便宜上、ここではベクターを配列として扱います。
// Rust では、長さを指定する場合[i32; 5]は配列であり、長さを指定しない場合&[i32]スライス
// Rust の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない
// Vector は Rust で一般に動的配列として使われる型
// 拡張 extend() メソッド実装しやすくするため、以下では vector を配列(array)として扱う
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 };
```
@@ -113,18 +113,32 @@
=== "Kotlin"
```kotlin title="array.kt"
/* 配列を初期化する */
var arr = IntArray(5) // { 0, 0, 0, 0, 0 }
var nums = intArrayOf(1, 3, 2, 5, 4)
```
=== "Ruby"
```ruby title="array.rb"
# 配列を初期化する
arr = Array.new(5, 0)
nums = [1, 3, 2, 5, 4]
```
??? pythontutor "実行の可視化"
https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### 要素へのアクセス
配列内の要素は連続したメモリ空間に格納されるため、要素のメモリアドレス計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。
配列要素は連続したメモリ領域に格納されるため、要素のメモリアドレス計算は非常に容易です。配列のメモリアドレス(先頭要素のメモリアドレス)とある要素のインデックスが与えられれば、下図の式を使ってその要素のメモリアドレスを計算でき、直接その要素にアクセスできます。
![配列要素のメモリアドレス計算](array.assets/array_memory_location_calculation.png)
![配列要素のメモリアドレス計算](array.assets/array_memory_location_calculation.png)
の図で観察されるように、配列のインデックスは慣例的に$0$から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常$1$から始まるためですが、アドレス計算公式内では、**インデックス本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは$0$、そのインデックスが$0$であることを検証しています。
図を見ると、配列の最初の要素のインデックスは $0$ であり、これは少し直感に反するように思えます。というのも、$1$ から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックス本質メモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは $0$ であるため、そのインデックスが $0$ なのは妥当です。
配列内の要素へのアクセスは非常に効率的で、$O(1)$時間で任意の要素にランダムアクセスできます。
配列では要素へのアクセスは非常に効率的であり、$O(1)$ 時間で任意の要素にランダムアクセスできます。
```src
[file]{array}-[class]{}-[func]{random_access}
@@ -132,11 +146,11 @@
### 要素の挿入
配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。
配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。
![配列要素挿入の例](array.assets/array_insert_element.png)
![配列への要素挿入の例](array.assets/array_insert_element.png)
配列の長さ固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題解決する方法は「リスト」の章で探求されます。
注意すべき点として、配列の長さ固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題解決は「リスト」の章で扱います。
```src
[file]{array}-[class]{}-[func]{insert}
@@ -144,25 +158,25 @@
### 要素の削除
同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。
同様に、下図のように、インデックス $i$ の要素を削除したい場合は、インデックス $i$ より後ろの要素をすべて 1 つずつ前へずらす必要があります。
![配列要素削除の例](array.assets/array_remove_element.png)
![配列からの要素削除の例](array.assets/array_remove_element.png)
削除後、元の最後の要素は「意味ない」ものになるため、特定の修正は必要ないことに注意してください
注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません
```src
[file]{array}-[class]{}-[func]{remove}
```
要約すると、配列の挿入と削除操作には以下の欠点があります
全体として見ると、配列の挿入と削除には次の欠点があります
- **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$、ここで$n$は配列の長さです。
- **要素の損失**:配列の長さが固定されているため、挿入時に配列の容量を超え要素は失われます。
- **メモリの無駄**より長い配列を初期化して前部分のみを利用すると、挿入時に「意味のない」末尾要素が生じ、メモリ空間の無駄につながります。
- **時間計算量が高い**:配列の挿入と削除の平均時間計算量はいずれも $O(n)$ であり、ここで $n$ は配列です。
- **要素が失われる**:配列の長さは不変であるため、要素を挿入すると配列長の範囲を超え要素は失われます。
- **メモリの浪費**やや長めの配列を初期化して先頭部分だけを使うこともでき、この場合データ挿入時に失われる末尾要素はすべて「無意味」ですが、その代わり一部のメモリ領域が無駄になります。
### 配列の走査
ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます
ほとんどのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます
```src
[file]{array}-[class]{}-[func]{traverse}
@@ -170,9 +184,9 @@
### 要素の検索
配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。
配列内で指定した要素を探すには、配列を走査し、各反復で要素値が一致するかを判定し、一致したら対応するインデックスを出力します。
配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。
配列は線形データ構造であるため、上記の検索操作は「線形探索」と呼ばれます。
```src
[file]{array}-[class]{}-[func]{find}
@@ -180,34 +194,34 @@
### 配列の拡張
複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。
複雑なシステム環境では、配列の後方にあるメモリ領域が利用可能であることをプログラム側で保証できず、そのため安全に配列容量を拡張できません。したがって、ほとんどのプログラミング言語では、**配列の長さは不変です**
配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は$O(n)$で、大きな配列では時間がかかる可能性があります。コードは以下の通りです
配列を拡張したい場合は、より大きな新しい配列を作、元の配列要素を順に新配列へコピーする必要があります。これは $O(n)$ の操作であり、配列が大きい場合は非常に時間がかかります。コードは次のとおりです
```src
[file]{array}-[class]{}-[func]{extend}
```
## 配列の利点と
## 配列の利点と限
配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作効率を最適化するために活用できる実質的な事前情報を提供します。
配列は連続したメモリ領域に格納され、要素の型も同一です。この方法には豊富な事前情報が含まれており、システムはそれらを利用してデータ構造操作効率を最適化できます。
- **高い空間効率**:配列はデータのための連続したメモリブロックを割り当て、追加の構造オーバーヘッドの必要性を排除します。
- **ランダムアクセスサポート**:配列は任意の要素への$O(1)$時間アクセスを可能にします。
- **キャッシュ局所性**:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく周囲のデータもキャッシュ、高速キャッシュを利用して後続操作速度を向上させます。
- **空間効率が高い**:配列はデータ連続したメモリブロックを割り当てるため、追加の構造オーバーヘッドが不要です。
- **ランダムアクセスサポートする**:配列は任意の要素$O(1)$ 時間アクセスできます。
- **キャッシュ局所性**:配列要素にアクセスする、コンピュータはその要素だけでなく周囲のデータもキャッシュするため、高速キャッシュを利用して後続操作の実行速度を高められます。
しかし、連続空間格納は諸刃の剣で、以下の制限があります
連続領域への格納は諸刃の剣であり、次のような制約があります
- **挿入と削除の効率が低い**:配列に多くの要素が蓄積されると、要素の挿入や削除は大量の要素をシフトする必要があります。
- **固定長**:配列の長さは初期化後に固定されます。配列を拡張するにはすべてのデータを新しい配列コピーする必要があり、大きなコストがかかります。
- **空間の無駄**:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。
- **挿入と削除の効率が低い**:配列の要素が多い場合、挿入や削除は大量の要素を移動する必要があります。
- **長さが不変**:配列は初期化後に長さが固定され拡張するにはすべてのデータを新しい配列コピーする必要があり、コストが大きくなります。
- **空間の浪費**配列に割り当てたサイズが実際の必要量を上回る場合、余分な領域は無駄になります。
## 配列の典型的な応用
配列は基的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑データ構造の実装に役立ちます。
配列は基的で一般的なデータ構造であり、さまざまなアルゴリズムで頻繁に使われるだけでなく、多様な複雑データ構造の実装にも利用できます。
- **ランダムアクセス**配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。
- **ソートと索**:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。
- **ルックアップテーブル**配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。えば文字ASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。
- **機械学習**:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。
- **データ構造の実装**:配列はスタック、キュー、ハッシュ、ヒープ、グラフなど、様々なデータ構造実装するための構成要素として機能します。えば、グラフの隣接行列表現は本質的に二次元配列です。
- **ランダムアクセス**いくつかのサンプルをランダムに抽出したい場合、配列に格納してランダムな系列を生成し、インデックスに基づいてランダムサンプリングを行えます。
- **ソートと索**:配列はソートアルゴリズムと探索アルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは主に配列上で行われます。
- **ルックアップテーブル**ある要素やその対応関係を高速に調べる必要がある場合、配列をルックアップテーブルとして使えます。たとえば文字から ASCII コードへの対応を実装したいなら、文字の ASCII コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
- **機械学習**:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
- **データ構造の実装**:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどデータ構造実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
@@ -4,6 +4,6 @@
!!! abstract
データ構造の世界は頑丈なレンガの壁に似ています。
データ構造の世界は、まるで重厚なれんがの壁のようです。
配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。
配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。
@@ -1,20 +1,20 @@
# 連結リスト
メモリ空間はすべてのプログラム間で共有されるリソースです。複雑なシステム環境では、使用可能なメモリがメモリ空間全体に分散している可能性があります。配列に割り当てられるメモリは連続している必要があることを理解していますが、非常に大きな配列の場合、十分な大きさの連続メモリ空間を見つけるのは困難な場合があります。ここで、連結リストの柔軟な利点が明らかになります。
メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。
<u>連結リスト</u>は線形データ構造であり、各要素はノードオブジェクトでノードは「参照」を通じて相互接続されています。これらの参照は後続ノードのメモリアドレスを保持し、1つのノードから次のノードへのナビゲーションを可能にします。
<u>連結リストlinked list</u>は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。
連結リストの設計では、ノードを連続するメモリアドレスを必要とせずに、メモリ位置全体に分散配置することができます
連結リストの設計では、ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません
![連結リストの定義と格納方](linked_list.assets/linkedlist_definition.png)
![連結リストの定義と格納方](linked_list.assets/linkedlist_definition.png)
上図に示すように、連結リストの基本的な構成要素は<u>ノード</u>オブジェクトです。各ノードは2つの主要なコンポーネントで構成されています:ノードの「値」と次のノードへの「参照」す。
上図を見ると、連結リストの構成単位は<u>ノードnode</u>オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。
- 連結リストの最初のノードは「ヘッドノード」、最後のノードは「テールノード」です。
- テールノードは「null」を指し、Javaでは`null`C++では`nullptr`Pythonでは`None`として指定されます。
- C、C++、Go、Rustなどポインタをサポートする言語では、の「参照」は通常「ポインタ」として実装されます。
- 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
- 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ `null``nullptr``None` と表記します。
- C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。
以下のコードが示すように、連結リスト`ListNode`は値を保持するだけでなく、追加の参照(またはポインタ)も維持する必要があります。したがって、**連結リストは同じ量のデータを格納する場合、配列より多くのメモリ空間を占有します**。
以下のコードが示すように、連結リストノード `ListNode` は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、**連結リストは配列より多くのメモリ空間を消費します**。
=== "Python"
@@ -22,7 +22,7 @@
class ListNode:
"""連結リストノードクラス"""
def __init__(self, val: int):
self.val: int = val # ノード値
self.val: int = val # ノード
self.next: ListNode | None = None # 次のノードへの参照
```
@@ -31,7 +31,7 @@
```cpp title=""
/* 連結リストノード構造体 */
struct ListNode {
int val; // ノード値
int val; // ノード
ListNode *next; // 次のノードへのポインタ
ListNode(int x) : val(x), next(nullptr) {} // コンストラクタ
};
@@ -42,7 +42,7 @@
```java title=""
/* 連結リストノードクラス */
class ListNode {
int val; // ノード値
int val; // ノード
ListNode next; // 次のノードへの参照
ListNode(int x) { val = x; } // コンストラクタ
}
@@ -52,8 +52,8 @@
```csharp title=""
/* 連結リストノードクラス */
class ListNode(int x) { // コンストラクタ
int val = x; // ノード値
class ListNode(int x) { //コンストラクタ
int val = x; // ノード
ListNode? next; // 次のノードへの参照
}
```
@@ -63,11 +63,11 @@
```go title=""
/* 連結リストノード構造体 */
type ListNode struct {
Val int // ノード値
Val int // ノード
Next *ListNode // 次のノードへのポインタ
}
// NewListNode コンストラクタ新しい連結リストを作成
// NewListNode コンストラクタ新しい連結リストを作成する
func NewListNode(val int) *ListNode {
return &ListNode{
Val: val,
@@ -81,7 +81,7 @@
```swift title=""
/* 連結リストノードクラス */
class ListNode {
var val: Int // ノード値
var val: Int // ノード
var next: ListNode? // 次のノードへの参照
init(x: Int) { // コンストラクタ
@@ -96,7 +96,7 @@
/* 連結リストノードクラス */
class ListNode {
constructor(val, next) {
this.val = (val === undefined ? 0 : val); // ノード値
this.val = (val === undefined ? 0 : val); // ノード
this.next = (next === undefined ? null : next); // 次のノードへの参照
}
}
@@ -110,7 +110,7 @@
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val; // ノード値
this.val = val === undefined ? 0 : val; // ノード
this.next = next === undefined ? null : next; // 次のノードへの参照
}
}
@@ -121,7 +121,7 @@
```dart title=""
/* 連結リストノードクラス */
class ListNode {
int val; // ノード値
int val; // ノード
ListNode? next; // 次のノードへの参照
ListNode(this.val, [this.next]); // コンストラクタ
}
@@ -135,7 +135,7 @@
/* 連結リストノードクラス */
#[derive(Debug)]
struct ListNode {
val: i32, // ノード値
val: i32, // ノード
next: Option<Rc<RefCell<ListNode>>>, // 次のノードへのポインタ
}
```
@@ -145,7 +145,7 @@
```c title=""
/* 連結リストノード構造体 */
typedef struct ListNode {
int val; // ノード値
int val; // ノード
struct ListNode *next; // 次のノードへのポインタ
} ListNode;
@@ -162,19 +162,39 @@
=== "Kotlin"
```kotlin title=""
/* 連結リストノードクラス */
// コンストラクタ
class ListNode(x: Int) {
val _val: Int = x // ノードの値
val next: ListNode? = null // 次のノードへの参照
}
```
## 連結リストの一般的な操作
=== "Ruby"
```ruby title=""
# 連結リストノードクラス
class ListNode
attr_accessor :val # ノードの値
attr_accessor :next # 次のノードへの参照
def initialize(val=0, next_node=nil)
@val = val
@next = next_node
end
end
```
## 連結リストの基本操作
### 連結リストの初期化
連結リストの構築は2段階のプロセスです:まず各ノードオブジェクトを初期化し、にノード間の参照リンクを形成します。初期化後、ヘッドノードから`next`参照をたどってすべてのノードを順次巡回できます。
連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で `next` をたどってすべてのノードに順にアクセスできます。
=== "Python"
```python title="linked_list.py"
# 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化
# 各ノードを初期化
n0 = ListNode(1)
n1 = ListNode(3)
@@ -191,7 +211,7 @@
=== "C++"
```cpp title="linked_list.cpp"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
ListNode* n0 = new ListNode(1);
ListNode* n1 = new ListNode(3);
@@ -208,7 +228,7 @@
=== "Java"
```java title="linked_list.java"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
ListNode n0 = new ListNode(1);
ListNode n1 = new ListNode(3);
@@ -225,7 +245,7 @@
=== "C#"
```csharp title="linked_list.cs"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
ListNode n0 = new(1);
ListNode n1 = new(3);
@@ -242,7 +262,7 @@
=== "Go"
```go title="linked_list.go"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
n0 := NewListNode(1)
n1 := NewListNode(3)
@@ -259,7 +279,7 @@
=== "Swift"
```swift title="linked_list.swift"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
let n0 = ListNode(x: 1)
let n1 = ListNode(x: 3)
@@ -276,7 +296,7 @@
=== "JS"
```javascript title="linked_list.js"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
const n0 = new ListNode(1);
const n1 = new ListNode(3);
@@ -293,7 +313,7 @@
=== "TS"
```typescript title="linked_list.ts"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
const n0 = new ListNode(1);
const n1 = new ListNode(3);
@@ -310,7 +330,7 @@
=== "Dart"
```dart title="linked_list.dart"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\
// 各ノードを初期化
ListNode n0 = ListNode(1);
ListNode n1 = ListNode(3);
@@ -327,7 +347,7 @@
=== "Rust"
```rust title="linked_list.rs"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));
let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));
@@ -345,7 +365,7 @@
=== "C"
```c title="linked_list.c"
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
ListNode* n0 = newListNode(1);
ListNode* n1 = newListNode(3);
@@ -362,18 +382,50 @@
=== "Kotlin"
```kotlin title="linked_list.kt"
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */
// 各ノードを初期化
val n0 = ListNode(1)
val n1 = ListNode(3)
val n2 = ListNode(2)
val n3 = ListNode(5)
val n4 = ListNode(4)
// ノード間の参照を構築
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
配列全体は1つの変数です。例えば、配列`nums`には`nums[0]`、`nums[1]`などの要素が含まれますが、連結リストは複数の異なるノードオブジェクトで構成されています。**通常、連結リストはそのヘッドノードで参照されます**。例えば、前のコードスニペットの連結リストは`n0`として参照されます。
=== "Ruby"
```ruby title="linked_list.rb"
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化
# 各ノードを初期化
n0 = ListNode.new(1)
n1 = ListNode.new(3)
n2 = ListNode.new(2)
n3 = ListNode.new(5)
n4 = ListNode.new(4)
# ノード間の参照を構築
n0.next = n1
n1.next = n2
n2.next = n3
n3.next = n4
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
配列全体は 1 つの変数であり、たとえば配列 `nums` には `nums[0]` や `nums[1]` などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。**通常、先頭ノードを連結リストの代名詞として扱います**。たとえば上記のコードの連結リストは `n0` と表せます。
### ノードの挿入
連結リストノード挿入するのは非常に簡単です。下図に示すように、隣接する2つのノード`n0``n1`の間に新しいノード`P`を挿入することを目指すとします。**これは2つのノード参照(ポインタ)を変更するだけで実現でき**、時間計算量は$O(1)$です。
連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード `n0``n1` の間に新しいノード `P` を挿入したいとします。**このとき 2 つのノード参照(ポインタ)を変更するだけでよく**、時間計算量は $O(1)$ です。
比較すると、配列に要素を挿入する時間計算量は$O(n)$であり、大量のデータを扱う場合には効率が悪くなります。
これに対して、配列に要素を挿入する時間計算量は $O(n)$ であり、データ量が大きい場合の効率は低くなります。
![連結リストノード挿入例](linked_list.assets/linkedlist_insert_node.png)
![連結リストへのノード挿入例](linked_list.assets/linkedlist_insert_node.png)
```src
[file]{linked_list}-[class]{}-[func]{insert}
@@ -381,11 +433,11 @@
### ノードの削除
下図に示すように、連結リストからノード削除することも非常に簡単で、**1つのノードの参照(ポインタ)を変更するだけです**。
下図に示すように、連結リストでのノード削除も非常に簡単で、**1 つのノードの参照(ポインタ)を変更するだけで済みます**。
重要な点は、ノード`P`が削除された後も`n1`を指し続けていることですが、連結リストの巡回中にはアクセスできなくなることです。これは事実上、`P`が連結リストの一部ではなくなったことを意味します
なお、削除操作が完了した後もノード `P` は依然として `n1` を指していますが、実際にはこの連結リストをたどっても `P` へはアクセスできません。つまり、`P` はすでにこの連結リストには属していません
![連結リストノード削除](linked_list.assets/linkedlist_remove_node.png)
![連結リストノード削除](linked_list.assets/linkedlist_remove_node.png)
```src
[file]{linked_list}-[class]{}-[func]{remove}
@@ -393,15 +445,15 @@
### ノードへのアクセス
**連結リストでノードへのアクセスは効率が悪いです**。前述したように、配列任意の要素には$O(1)$時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノード見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの$i$番目のノードにアクセスするには、プログラムは$i - 1$個のノードを反復処理する必要あり、時間計算量は$O(n)$になります。
**連結リストでノードアクセスする効率は低い**です。前節で述べたように、配列では任意の要素$O(1)$ 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、1 つずつ後ろへたどって目的のノード見つる必要があります。つまり、連結リストの$i$ ノードにアクセスするには $i - 1$ 回のループが必要あり、時間計算量は $O(n)$す。
```src
[file]{linked_list}-[class]{}-[func]{access}
```
### ノードの
### ノードの
連結リストを巡回して、値が`target`に一致するノードを見つけ、連結リスト内でのそのノードのインデックスを出力します。この手順も線形検索の例です。対応するコードは以下のとおりです
連結リストを走査し、その中から値が `target` のノードを探し、そのノードの連結リスト内でのインデックスを出力します。この処理も線形探索に属します。コードはのとおりです
```src
[file]{linked_list}-[class]{}-[func]{find}
@@ -409,26 +461,26 @@
## 配列 vs. 連結リスト
表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。
次の表は配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。
<p align="center"> 表 <id> &nbsp; 配列と連結リストの効率比較 </p>
| | 配列 | 連結リスト |
| ------------------ | ------------------------------------------------ | ----------------------- |
| 格納方式 | 連続メモリ空間 | 分散メモリ空間 |
| 容量拡張 | 固定長 | 柔軟拡張 |
| メモリ効率 | 要素あたりのメモリ少、潜在的な空間無駄 | 要素あたりのメモリ |
| 要素へのアクセス | $O(1)$ | $O(n)$ |
| 要素の追加 | $O(n)$ | $O(1)$ |
| 要素の削除 | $O(n)$ | $O(1)$ |
| | 配列 | 連結リスト |
| -------- | ------------------------------ | -------------- |
| 格納方式 | 連続したメモリ空間 | 分散したメモリ空間 |
| 容量拡張 | 長さは不変 | 柔軟拡張可能 |
| メモリ効率 | 要素のメモリ消費は少ないが、空間無駄にする可能性がある | 要素のメモリ消費が多い |
| 要素へのアクセス | $O(1)$ | $O(n)$ |
| 要素の追加 | $O(n)$ | $O(1)$ |
| 要素の削除 | $O(n)$ | $O(1)$ |
## 連結リストの一般的な種類
## 一般的な連結リストの種類
下図に示すように、連結リストには3つの一般的な種類があります。
下図に示すように、一般的な連結リストの種類は 3 つあります。
- **単方向連結リスト**これは前述した標準的な連結リストです。単方向連結リストのノードは値と次のノードへの参照が含まれます。最初のノードはヘッドノードと呼ばれ、null(`None`を指す最後のノードはテールノードです。
- **循環連結リスト**これは単方向連結リストのテールノードがヘッドノードを指してループを作ることで形成されます。循環連結リストでは、任意のノードがヘッドノードとして機能できます。
- **双方向連結リスト**:単方向連結リストとは対照的に、双方向連結リストは2つの方向参照を維持します。ノードには後続(次のノード)と前任者(前のノード)の両方への参照(ポインタ)が含まれます。方向連結リストはどちらの方向にも巡回できるより多くの柔軟性を提供しますが、より多くのメモリ空間も消費します。
- **単方向連結リスト**:前述した通常の連結リストのことです。単方向連結リストのノードは値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 `None` を指します。
- **循環連結リスト**:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
- **双方向連結リスト**:単方向連結リストと比べて、双方向連結リストは 2 方向参照を記録します。双方向連結リストのノード定義には後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
=== "Python"
@@ -436,9 +488,9 @@
class ListNode:
"""双方向連結リストノードクラス"""
def __init__(self, val: int):
self.val: int = val # ノード値
self.val: int = val # ノード
self.next: ListNode | None = None # 後続ノードへの参照
self.prev: ListNode | None = None # 前ノードへの参照
self.prev: ListNode | None = None # 前ノードへの参照
```
=== "C++"
@@ -446,9 +498,9 @@
```cpp title=""
/* 双方向連結リストノード構造体 */
struct ListNode {
int val; // ノード値
int val; // ノード
ListNode *next; // 後続ノードへのポインタ
ListNode *prev; // 前ノードへのポインタ
ListNode *prev; // 前ノードへのポインタ
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // コンストラクタ
};
```
@@ -458,9 +510,9 @@
```java title=""
/* 双方向連結リストノードクラス */
class ListNode {
int val; // ノード値
ListNode next; // 次のノードへの参照
ListNode prev; // 前ノードへの参照
int val; // ノード
ListNode next; // 後続ノードへの参照
ListNode prev; // 前ノードへの参照
ListNode(int x) { val = x; } // コンストラクタ
}
```
@@ -470,9 +522,9 @@
```csharp title=""
/* 双方向連結リストノードクラス */
class ListNode(int x) { // コンストラクタ
int val = x; // ノード値
ListNode next; // 次のノードへの参照
ListNode prev; // 前ノードへの参照
int val = x; // ノード
ListNode next; // 後続ノードへの参照
ListNode prev; // 前ノードへの参照
}
```
@@ -481,12 +533,12 @@
```go title=""
/* 双方向連結リストノード構造体 */
type DoublyListNode struct {
Val int // ノード値
Val int // ノード
Next *DoublyListNode // 後続ノードへのポインタ
Prev *DoublyListNode // 前ノードへのポインタ
Prev *DoublyListNode // 前ノードへのポインタ
}
// NewDoublyListNode 初期化
// NewDoublyListNode 初期化
func NewDoublyListNode(val int) *DoublyListNode {
return &DoublyListNode{
Val: val,
@@ -501,9 +553,9 @@
```swift title=""
/* 双方向連結リストノードクラス */
class ListNode {
var val: Int // ノード値
var next: ListNode? // 次のノードへの参照
var prev: ListNode? // 前ノードへの参照
var val: Int // ノード
var next: ListNode? // 後続ノードへの参照
var prev: ListNode? // 前ノードへの参照
init(x: Int) { // コンストラクタ
val = x
@@ -517,9 +569,9 @@
/* 双方向連結リストノードクラス */
class ListNode {
constructor(val, next, prev) {
this.val = val === undefined ? 0 : val; // ノード値
this.val = val === undefined ? 0 : val; // ノード
this.next = next === undefined ? null : next; // 後続ノードへの参照
this.prev = prev === undefined ? null : prev; // 前ノードへの参照
this.prev = prev === undefined ? null : prev; // 前ノードへの参照
}
}
```
@@ -533,9 +585,9 @@
next: ListNode | null;
prev: ListNode | null;
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
this.val = val === undefined ? 0 : val; // ノード値
this.val = val === undefined ? 0 : val; // ノード
this.next = next === undefined ? null : next; // 後続ノードへの参照
this.prev = prev === undefined ? null : prev; // 前ノードへの参照
this.prev = prev === undefined ? null : prev; // 前ノードへの参照
}
}
```
@@ -545,9 +597,9 @@
```dart title=""
/* 双方向連結リストノードクラス */
class ListNode {
int val; // ノード値
ListNode next; // 次のノードへの参照
ListNode prev; // 前ノードへの参照
int val; // ノード
ListNode? next; // 後続ノードへの参照
ListNode? prev; // 前ノードへの参照
ListNode(this.val, [this.next, this.prev]); // コンストラクタ
}
```
@@ -561,9 +613,9 @@
/* 双方向連結リストノード型 */
#[derive(Debug)]
struct ListNode {
val: i32, // ノード値
val: i32, // ノード
next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ
prev: Option<Rc<RefCell<ListNode>>>, // 前ノードへのポインタ
prev: Option<Rc<RefCell<ListNode>>>, // 前ノードへのポインタ
}
/* コンストラクタ */
@@ -583,14 +635,14 @@
```c title=""
/* 双方向連結リストノード構造体 */
typedef struct ListNode {
int val; // ノード値
int val; // ノード
struct ListNode *next; // 後続ノードへのポインタ
struct ListNode *prev; // 前ノードへのポインタ
struct ListNode *prev; // 前ノードへのポインタ
} ListNode;
/* コンストラクタ */
ListNode *newListNode(int val) {
ListNode *node, *next;
ListNode *node;
node = (ListNode *) malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
@@ -602,26 +654,49 @@
=== "Kotlin"
```kotlin title=""
/* 双方向連結リストノードクラス */
// コンストラクタ
class ListNode(x: Int) {
val _val: Int = x // ノードの値
val next: ListNode? = null // 後続ノードへの参照
val prev: ListNode? = null // 前駆ノードへの参照
}
```
![連結リストの一般的な種類](linked_list.assets/linkedlist_common_types.png)
=== "Ruby"
```ruby title=""
# 双方向連結リストノードクラス
class ListNode
attr_accessor :val # ノードの値
attr_accessor :next # 後続ノードへの参照
attr_accessor :prev # 前駆ノードへの参照
def initialize(val=0, next_node=nil, prev_node=nil)
@val = val
@next = next_node
@prev = prev_node
end
end
```
![一般的な連結リストの種類](linked_list.assets/linkedlist_common_types.png)
## 連結リストの典型的な応用
単方向連結リストは、スタック、キュー、ハッシュ表、グラフの実装によく使用されます。
単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。
- **スタックとキュー**単方向連結リストで、挿入と削除が同じ端で行われる場合、スタック(後入先出)のように動作します。逆に、挿入が一方の端で、削除もう一方の端で行われる場合、キュー(先入先出)のように機能します。
- **ハッシュ表**:連結リストは、ハッシュ衝突を解決する人気の方法である連鎖法で使用されます。ここでは、すべての衝突した要素が連結リストにグループ化されます。
- **グラフ**:グラフ表現の標準的な方法である隣接リストは、各グラフ頂点を連結リストに関連付けます。このリストには、対応する頂点に接続された頂点を表す要素が含まれます。
- **スタックとキュー**挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除もう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
- **ハッシュテーブル**:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
- **グラフ**隣接リストはグラフ表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表ます。
双方向連結リストは、前後の要素への高速アクセスが必要なシナリオに最適です。
双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。
- **高度なデータ構造**赤黒木やB木などの構造では、ノードの親へのアクセスが重要です。これはノードに親ノードへの参照を組み込むことで実現され、双方向連結リストに似ています。
- **ブラウザ履歴**:Webブラウザでは、双方向連結リストにより、ユーザーが前進または後退ボタンをクリックしたときの訪問ページの履歴ナビゲーションが容易になります。
- **LRUアルゴリズム**双方向連結リストは、最近最少使用(LRU)キャッシュ削除アルゴリズムに適しており、最近最使用データの迅速な識別と、高速なノード追加削除を可能にします。
- **高度なデータ構造**たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これはノードに親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
- **ブラウザ履歴**Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
- **LRU アルゴリズム**キャッシュ淘汰(LRU)アルゴリズムでは、最近最使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加削除も必要です。そのため、双方向連結リストが非常に適しています。
循環連結リストは、オペレーティングシステムのリソーススケジューリングなど、周期的な操作が必要なアプリケーションに最適です。
循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。
- **ラウンドロビンスケジューリングアルゴリズム**:オペレーティングシステムでは、ラウンドロビンスケジューリングアルゴリズムは一般的なCPUスケジューリング方法であり、プロセスのグループを循環する必要があります。各プロセスにはタイムスライスが割り当てられ、期限切れになるとCPUは次のプロセスに回転します。この循環操作は循環連結リストを使用して効率的に実現でき、すべてのプロセス間で公平かつ時分割システムを可能にします。
- **データバッファ**循環連結リストは、オーディオやビデオプレーヤーなどのデータバッファでも使用され、データストリーム複数のバッファブロックに分割され、シームレス再生のために循環方式で配置されます。
- **ラウンドロビン時間片スケジューリングアルゴリズム**:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切るとCPU は次のプロセスへ切り替わります。この循環操作は循環連結リストで実現できます。
- **データバッファ**一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリーム複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
+259 -131
View File
@@ -1,21 +1,21 @@
# リスト
<u>リスト</u>は、要素へのアクセス、更、追加、削除、走査などの操作をサポートする、順序付けられた要素のコレクションを表す抽象的なデータ構造の概念であり、ユーザーが容量制限を考慮する必要がありません。リストは連結リストまたは配列に基づいて実装できます
<u>リストlist</u>は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装でき
- 連結リストは本質的にリストとして機能し、要素の追加削除、検索、変更の操作をサポートし、サイズを動的に調整する柔軟性があります
- 配列もこれらの操作をサポートしますが、長さが不変であるため、長さ制限のあるリストと考えることができます
- 連結リストは本質的にリストと見なすことができ、要素の追加削除・参照・更新をサポートし、柔軟に動的拡張できる
- 配列も要素の追加・削除・参照・更新をサポートすが、長さが不変であるため、長さ制限のあるリストとしか見なせない
配列を使用してリストを実装する場合、**長さ不変性によりリストの実用性が低下します**。これは、事前に格納するデータ量を予測することが困難な場合が多く、適切なリスト長を選択することが困難であるためです。長さが小さすぎると要件を満たない可能性があり、大きすぎるとメモリ空間を無駄にする可能性があります
配列リストを実装する場合、**長さ不変である性質によってリストの実用性が低下す**。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たない可能性が高く、大きすぎるとメモリ空間の浪費を招く
この問題を解決するために、<u>動的配列</u>を使用してリストを実装できます。これは配列の利点を継承し、プログラム実行中に動的拡張できます
この問題を解決するために、<u>動的配列dynamic array</u>を用いてリストを実装でき。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的拡張を行える
実際、**多くのプログラミング言語の標準ライブラリは動的配列を使用してリストを実装しています**。えば、Python`list`、Java`ArrayList`、C++`vector`、C#の`List`などで。以の議論では、「リスト」と「動的配列」を同義の概念として扱います
実際には、**多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている**。たとえば、Python`list` 、Java`ArrayList` 、C++`vector` 、C# の `List` などである。以の議論では、「リスト」と「動的配列」を同概念として扱
## リストの一般的な操作
## リストの基本操作
### リストの初期化
通常「初期値なし」と「初期値あり」の2つの初期化方法を使用します
通常「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる
=== "Python"
@@ -31,7 +31,7 @@
```cpp title="list.cpp"
/* リストを初期化 */
// 注意: C++ではvectorがここで説明されているnumsに相当しま
// なお、C++ では vector が本稿でいう nums に相当す
// 初期値なし
vector<int> nums1;
// 初期値あり
@@ -44,7 +44,7 @@
/* リストを初期化 */
// 初期値なし
List<Integer> nums1 = new ArrayList<>();
// 初期値あり(要素型はint[]のラッパークラスInteger[]である必要があります
// 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
List<Integer> nums = new ArrayList<>(Arrays.asList(numbers));
```
@@ -123,133 +123,168 @@
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* リストを初期化 */
// 初期値なし
var nums1 = listOf<Int>()
// 初期値あり
var numbers = arrayOf(1, 3, 2, 5, 4)
var nums = numbers.toMutableList()
```
=== "Ruby"
```ruby title="list.rb"
# リストを初期化
# 初期値なし
nums1 = []
# 初期値あり
nums = [1, 3, 2, 5, 4]
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### 要素へのアクセス
リスト本質的に配列であるため、$O(1)$時間で要素にアクセスし更新することができ、非常に効率的です
リスト本質配列であるため、要素へのアクセスと更新は $O(1)$ 時間で行え、効率が高い
=== "Python"
```python title="list.py"
# 要素にアクセス
num: int = nums[1] # インデックス1の要素にアクセス
num: int = nums[1] # インデックス 1 の要素にアクセス
# 要素を更新
nums[1] = 0 # インデックス1の要素を0に更新
nums[1] = 0 # インデックス 1 の要素を 0 に更新
```
=== "C++"
```cpp title="list.cpp"
/* 要素にアクセス */
int num = nums[1]; // インデックス1の要素にアクセス
int num = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "Java"
```java title="list.java"
/* 要素にアクセス */
int num = nums.get(1); // インデックス1の要素にアクセス
int num = nums.get(1); // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums.set(1, 0); // インデックス1の要素を0に更新
nums.set(1, 0); // インデックス 1 の要素を 0 に更新
```
=== "C#"
```csharp title="list.cs"
/* 要素にアクセス */
int num = nums[1]; // インデックス1の要素にアクセス
int num = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "Go"
```go title="list_test.go"
/* 要素にアクセス */
num := nums[1] // インデックス1の要素にアクセス
num := nums[1] // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0 // インデックス1の要素を0に更新
nums[1] = 0 // インデックス 1 の要素を 0 に更新
```
=== "Swift"
```swift title="list.swift"
/* 要素にアクセス */
let num = nums[1] // インデックス1の要素にアクセス
let num = nums[1] // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0 // インデックス1の要素を0に更新
nums[1] = 0 // インデックス 1 の要素を 0 に更新
```
=== "JS"
```javascript title="list.js"
/* 要素にアクセス */
const num = nums[1]; // インデックス1の要素にアクセス
const num = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "TS"
```typescript title="list.ts"
/* 要素にアクセス */
const num: number = nums[1]; // インデックス1の要素にアクセス
const num: number = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "Dart"
```dart title="list.dart"
/* 要素にアクセス */
int num = nums[1]; // インデックス1の要素にアクセス
int num = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "Rust"
```rust title="list.rs"
/* 要素にアクセス */
let num: i32 = nums[1]; // インデックス1の要素にアクセス
let num: i32 = nums[1]; // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0; // インデックス1の要素を0に更新
nums[1] = 0; // インデックス 1 の要素を 0 に更新
```
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* 要素にアクセス */
val num = nums[1] // インデックス 1 の要素にアクセス
/* 要素を更新 */
nums[1] = 0 // インデックス 1 の要素を 0 に更新
```
=== "Ruby"
```ruby title="list.rb"
# 要素にアクセス
num = nums[1] # インデックス 1 の要素にアクセス
# 要素を更新
nums[1] = 0 # インデックス 1 の要素を 0 に更新
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### 要素の挿入と削除
配列と比較して、リストは要素追加削除においてより柔軟性を提供します。リスト末尾への要素追加は$O(1)$操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は$O(n)$です
配列と比て、リストは要素を自由に追加削除できる。リスト末尾への要素追加の時間計算量は $O(1)$ だが、要素の挿入と削除の効率は依然として配列と同じで、時間計算量は $O(n)$ である
=== "Python"
@@ -264,11 +299,11 @@
nums.append(5)
nums.append(4)
# 中に要素を挿入
nums.insert(3, 6) # インデックス3に数値6を挿入
# 中に要素を挿入
nums.insert(3, 6) # インデックス 3 に数値 6 を挿入
# 要素を削除
nums.pop(3) # インデックス3の要素を削除
nums.pop(3) # インデックス 3 の要素を削除
```
=== "C++"
@@ -284,11 +319,11 @@
nums.push_back(5);
nums.push_back(4);
/* 中に要素を挿入 */
nums.insert(nums.begin() + 3, 6); // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums.insert(nums.begin() + 3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.erase(nums.begin() + 3); // インデックス3の要素を削除
nums.erase(nums.begin() + 3); // インデックス 3 の要素を削除
```
=== "Java"
@@ -304,11 +339,11 @@
nums.add(5);
nums.add(4);
/* 中に要素を挿入 */
nums.add(3, 6); // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums.add(3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.remove(3); // インデックス3の要素を削除
nums.remove(3); // インデックス 3 の要素を削除
```
=== "C#"
@@ -324,11 +359,11 @@
nums.Add(5);
nums.Add(4);
/* 中に要素を挿入 */
nums.Insert(3, 6);
/* 中に要素を挿入 */
nums.Insert(3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.RemoveAt(3);
nums.RemoveAt(3); // インデックス 3 の要素を削除
```
=== "Go"
@@ -344,11 +379,11 @@
nums = append(nums, 5)
nums = append(nums, 4)
/* 中に要素を挿入 */
nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums = append(nums[:3], nums[4:]...) // インデックス3の要素を削除
nums = append(nums[:3], nums[4:]...) // インデックス 3 の要素を削除
```
=== "Swift"
@@ -364,11 +399,11 @@
nums.append(5)
nums.append(4)
/* 中に要素を挿入 */
nums.insert(6, at: 3) // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums.insert(6, at: 3) // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.remove(at: 3) // インデックス3の要素を削除
nums.remove(at: 3) // インデックス 3 の要素を削除
```
=== "JS"
@@ -384,11 +419,11 @@
nums.push(5);
nums.push(4);
/* 中に要素を挿入 */
nums.splice(3, 0, 6);
/* 中に要素を挿入 */
nums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.splice(3, 1);
nums.splice(3, 1); // インデックス 3 の要素を削除
```
=== "TS"
@@ -404,11 +439,11 @@
nums.push(5);
nums.push(4);
/* 中に要素を挿入 */
nums.splice(3, 0, 6);
/* 中に要素を挿入 */
nums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.splice(3, 1);
nums.splice(3, 1); // インデックス 3 の要素を削除
```
=== "Dart"
@@ -424,11 +459,11 @@
nums.add(5);
nums.add(4);
/* 中に要素を挿入 */
nums.insert(3, 6); // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums.insert(3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.removeAt(3); // インデックス3の要素を削除
nums.removeAt(3); // インデックス 3 の要素を削除
```
=== "Rust"
@@ -444,38 +479,76 @@
nums.push(5);
nums.push(4);
/* 中に要素を挿入 */
nums.insert(3, 6); // インデックス3に数値6を挿入
/* 中に要素を挿入 */
nums.insert(3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.remove(3); // インデックス3の要素を削除
nums.remove(3); // インデックス 3 の要素を削除
```
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* リストをクリア */
nums.clear();
/* 末尾に要素を追加 */
nums.add(1);
nums.add(3);
nums.add(2);
nums.add(5);
nums.add(4);
/* 途中に要素を挿入 */
nums.add(3, 6); // インデックス 3 に数値 6 を挿入
/* 要素を削除 */
nums.remove(3); // インデックス 3 の要素を削除
```
### リストの反復
=== "Ruby"
配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。
```ruby title="list.rb"
# リストをクリア
nums.clear
# 末尾に要素を追加
nums << 1
nums << 3
nums << 2
nums << 5
nums << 4
# 途中に要素を挿入
nums.insert(3, 6) # インデックス 3 に数値 6 を挿入
# 要素を削除
nums.delete_at(3) # インデックス 3 の要素を削除
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### リストの走査
配列と同様に、リストもインデックスに基づいて走査することも、各要素を直接走査することもできる。
=== "Python"
```python title="list.py"
# インデックスでリストを反復
# インデックスでリストを走査
count = 0
for i in range(len(nums)):
count += nums[i]
# リスト要素を直接反復
# リスト要素を直接走査
for num in nums:
count += num
```
@@ -483,13 +556,13 @@
=== "C++"
```cpp title="list.cpp"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0;
for (int num : nums) {
count += num;
@@ -499,13 +572,13 @@
=== "Java"
```java title="list.java"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums.get(i);
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
for (int num : nums) {
count += num;
}
@@ -514,13 +587,13 @@
=== "C#"
```csharp title="list.cs"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
int count = 0;
for (int i = 0; i < nums.Count; i++) {
count += nums[i];
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0;
foreach (int num in nums) {
count += num;
@@ -530,13 +603,13 @@
=== "Go"
```go title="list_test.go"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
count := 0
for i := 0; i < len(nums); i++ {
count += nums[i]
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0
for _, num := range nums {
count += num
@@ -546,13 +619,13 @@
=== "Swift"
```swift title="list.swift"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
var count = 0
for i in nums.indices {
count += nums[i]
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0
for num in nums {
count += num
@@ -562,13 +635,13 @@
=== "JS"
```javascript title="list.js"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
let count = 0;
for (let i = 0; i < nums.length; i++) {
count += nums[i];
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0;
for (const num of nums) {
count += num;
@@ -578,13 +651,13 @@
=== "TS"
```typescript title="list.ts"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
let count = 0;
for (let i = 0; i < nums.length; i++) {
count += nums[i];
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0;
for (const num of nums) {
count += num;
@@ -594,13 +667,13 @@
=== "Dart"
```dart title="list.dart"
/* インデックスでリストを反復 */
/* インデックスでリストを走査 */
int count = 0;
for (var i = 0; i < nums.length; i++) {
count += nums[i];
}
/* リスト要素を直接反復 */
/* リスト要素を直接走査 */
count = 0;
for (var num in nums) {
count += num;
@@ -610,13 +683,13 @@
=== "Rust"
```rust title="list.rs"
// インデックスでリストを反復
// インデックスでリストを走査
let mut _count = 0;
for i in 0..nums.len() {
_count += nums[i];
}
// リスト要素を直接反復
// リスト要素を直接走査
_count = 0;
for num in &nums {
_count += num;
@@ -626,96 +699,125 @@
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* インデックスでリストを走査 */
var count = 0
for (i in nums.indices) {
count += nums[i]
}
/* リストの要素を直接走査 */
for (num in nums) {
count += num
}
```
=== "Ruby"
```ruby title="list.rb"
# インデックスでリストを走査
count = 0
for i in 0...nums.length
count += nums[i]
end
# リストの要素を直接走査
count = 0
for num in nums
count += num
end
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### リストの連結
新しいリスト`nums1`が与えられたとき、それを元のリストの末尾に追加できます
新しいリスト `nums1` が与えられたとき、それを元のリストの末尾に連結できる
=== "Python"
```python title="list.py"
# 2つのリストを連結
# 2 つのリストを連結
nums1: list[int] = [6, 8, 7, 10, 9]
nums += nums1 # nums1numsの末尾に連結
nums += nums1 # リスト nums1nums の後ろに連結
```
=== "C++"
```cpp title="list.cpp"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
vector<int> nums1 = { 6, 8, 7, 10, 9 };
// nums1numsの末尾に連結
// リスト nums1nums の後ろに連結
nums.insert(nums.end(), nums1.begin(), nums1.end());
```
=== "Java"
```java title="list.java"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
List<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
nums.addAll(nums1); // nums1numsの末尾に連結
nums.addAll(nums1); // リスト nums1nums の後ろに連結
```
=== "C#"
```csharp title="list.cs"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
List<int> nums1 = [6, 8, 7, 10, 9];
nums.AddRange(nums1); // nums1numsの末尾に連結
nums.AddRange(nums1); // リスト nums1nums の後ろに連結
```
=== "Go"
```go title="list_test.go"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
nums1 := []int{6, 8, 7, 10, 9}
nums = append(nums, nums1...) // nums1numsの末尾に連結
nums = append(nums, nums1...) // リスト nums1nums の後ろに連結
```
=== "Swift"
```swift title="list.swift"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
let nums1 = [6, 8, 7, 10, 9]
nums.append(contentsOf: nums1) // nums1numsの末尾に連結
nums.append(contentsOf: nums1) // リスト nums1nums の後ろに連結
```
=== "JS"
```javascript title="list.js"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
const nums1 = [6, 8, 7, 10, 9];
nums.push(...nums1); // nums1numsの末尾に連結
nums.push(...nums1); // リスト nums1nums の後ろに連結
```
=== "TS"
```typescript title="list.ts"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
const nums1: number[] = [6, 8, 7, 10, 9];
nums.push(...nums1); // nums1numsの末尾に連結
nums.push(...nums1); // リスト nums1nums の後ろに連結
```
=== "Dart"
```dart title="list.dart"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
List<int> nums1 = [6, 8, 7, 10, 9];
nums.addAll(nums1); // nums1numsの末尾に連結
nums.addAll(nums1); // リスト nums1nums の後ろに連結
```
=== "Rust"
```rust title="list.rs"
/* 2つのリストを連結 */
/* 2 つのリストを連結 */
let nums1: Vec<i32> = vec![6, 8, 7, 10, 9];
nums.extend(nums1);
```
@@ -723,110 +825,136 @@
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* 2 つのリストを連結 */
val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()
nums.addAll(nums1) // リスト nums1 を nums の後ろに連結
```
### リストのソート
=== "Ruby"
リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。
```ruby title="list.rb"
# 2 つのリストを連結
nums1 = [6, 8, 7, 10, 9]
nums += nums1
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### リストをソート
リストのソートが完了すると、配列系アルゴリズム問題でよく問われる「二分探索」や「両ポインタ」アルゴリズムを使えるようになる。
=== "Python"
```python title="list.py"
# リストをソート
nums.sort() # ソート後、リスト要素は昇順になります
nums.sort() # ソート後、リスト要素は小さい順に並ぶ
```
=== "C++"
```cpp title="list.cpp"
/* リストをソート */
sort(nums.begin(), nums.end()); // ソート後、リスト要素は昇順になります
sort(nums.begin(), nums.end()); // ソート後、リスト要素は小さい順に並ぶ
```
=== "Java"
```java title="list.java"
/* リストをソート */
Collections.sort(nums); // ソート後、リスト要素は昇順になります
Collections.sort(nums); // ソート後、リスト要素は小さい順に並ぶ
```
=== "C#"
```csharp title="list.cs"
/* リストをソート */
nums.Sort(); // ソート後、リスト要素は昇順になります
nums.Sort(); // ソート後、リスト要素は小さい順に並ぶ
```
=== "Go"
```go title="list_test.go"
/* リストをソート */
sort.Ints(nums) // ソート後、リスト要素は昇順になります
sort.Ints(nums) // ソート後、リスト要素は小さい順に並ぶ
```
=== "Swift"
```swift title="list.swift"
/* リストをソート */
nums.sort() // ソート後、リスト要素は昇順になります
nums.sort() // ソート後、リスト要素は小さい順に並ぶ
```
=== "JS"
```javascript title="list.js"
/* リストをソート */
nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります
nums.sort((a, b) => a - b); // ソート後、リスト要素は小さい順に並ぶ
```
=== "TS"
```typescript title="list.ts"
/* リストをソート */
nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります
nums.sort((a, b) => a - b); // ソート後、リスト要素は小さい順に並ぶ
```
=== "Dart"
```dart title="list.dart"
/* リストをソート */
nums.sort(); // ソート後、リスト要素は昇順になります
nums.sort(); // ソート後、リスト要素は小さい順に並ぶ
```
=== "Rust"
```rust title="list.rs"
/* リストをソート */
nums.sort(); // ソート後、リスト要素は昇順になります
nums.sort(); // ソート後、リスト要素は小さい順に並ぶ
```
=== "C"
```c title="list.c"
// Cは組み込みの動的配列を提供していません
// Cは組み込みの動的配列がない
```
=== "Kotlin"
```kotlin title="list.kt"
/* リストをソート */
nums.sort() // ソート後、リスト要素は小さい順に並ぶ
```
=== "Ruby"
```ruby title="list.rb"
# リストをソート
nums = nums.sort { |a, b| a <=> b } # ソート後、リスト要素は小さい順に並ぶ
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
## リストの実装
多くのプログラミング言語にはJava、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます
多くのプログラミング言語にはリストが組み込まれており、たとえば Java、C++、Python などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる
リストがどのように動作するかの理解を深めるために、3つの重要な設計側面に焦点を当てて、簡略化されたリストの実装を試みます:
リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。
- **初期容量**配列に合理的な初期容量を選択します。この例では初期容量として10を選択します
- **サイズ記録**リスト内の現在の要素数を記録する変数`size`を宣言し、要素の挿入と削除リアルタイムに更新します。この変数により、リスト末尾を特定し、拡張が必要かどうかを判断できます
- **拡張メカニズム**:要素挿入時にリストが満杯に達した場合、拡張プロセスが必要です。これには拡張係数に基づいてより大きな配列を作成し、現在の配列からすべての要素を新しい配列に転送することが含まれます。この例では、拡張のたびに配列サイズを2倍にすることを規定します
- **初期容量**妥当な配列の初期容量を選。この例では 10 を初期容量として選ぶ
- **要素数の記録**`size` という変数を宣言して、現在のリスト要素数を記録し、要素の挿入と削除に応じてリアルタイムに更新す。この変数により、リスト末尾の位置を特定し、拡張が必要かどうかを判断でき
- **拡張機構**:要素挿入する時点でリスト容量がいっぱいなら、拡張が必要になる。まず拡張倍率に応じてより大きな配列を作成し、次に現在の配列の要素を順に新しい配列へ移す。この例では、配列を毎回以前の 2 倍に拡張する
```src
[file]{my_list}-[class]{my_list}-[func]{}
@@ -1,71 +1,71 @@
# メモリとキャッシュ *
この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました
本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています
実際、**物理構造はプログラムメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。
実際には、**物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し**、ひいてはアルゴリズムプログラム全体の性能に影響します。
## コンピュータ記憶装置
## コンピュータ記憶装置
コンピュータには3種類の記憶装置があります<u>ハードディスク</u>、<u>ランダムアクセスメモリ(RAM</u>、および<u>キャッシュメモリ</u>です。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。
コンピュータには種類の記憶装置があります<u>ハードディスクhard disk</u>、<u>メモリ(random-access memory, RAM</u>、<u>キャッシュcache memory</u>です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。
<p align="center"> 表 <id> &nbsp; コンピュータ記憶装置 </p>
<p align="center"> 表 <id> &nbsp; コンピュータ記憶装置 </p>
| | ハードディスク | メモリ | キャッシュ |
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| 用途 | OS、プログラム、ファイルなどのデータの長期保存 | 現在実行中のプログラム処理中のデータ一時保存 | 頻繁にアクセスされるデータ命令を保存し、CPUのメモリへのアクセス数を減 |
| 揮発性 | 電源オフ後もデータは失われない | 電源オフ後にデータは失われる | 電源オフ後にデータは失われる |
| 容量 | より大きい、TBレベル | より小さい、GBレベル | 非常に小さい、MBレベル |
| 速度 | より遅い、数百から数千MB/s | より高速、数十GB/s | 非常に速、数十から数百GB/s |
| 価格(USD | より安価、数セント/GB | より高価、数ドル/GB | 非常に高価、CPUと一緒に価格設定 |
| | ハードディスク | メモリ | キャッシュ |
| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | OS、プログラム、ファイルなど長期保存 | 実行中のプログラム処理中のデータ一時保存 | 頻繁にアクセスされるデータ命令を保存し、CPU のメモリアクセス数を減らす |
| 揮発性 | 電源後もデータは失われない | 電源後にデータは失われる | 電源後にデータは失われる |
| 容量 | 大きい、TB 級 | 小さい、GB 級 | 非常に小さい、MB 級 |
| 速度 | 遅い、数百数千 MB/s | 速い、数十 GB/s | 非常に速、数十数百 GB/s |
| 価格(人民元 | 比較的安価、数角〜数元 / GB | 比較的高価、数十〜数百元 / GB | 非常に高価、CPU と一体で価格設定される |
コンピュータ記憶システムは、下図に示すようピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者エンジニアによる慎重な検討の結果です。
コンピュータ記憶システムは、下図ようピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者エンジニアによる熟慮の末の設計です。
- **ハードディスクメモリ置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。
- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズ大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。
- **ハードディスクメモリ置き換えにくい**。まず、メモリ内のデータは電源後に失われるため、長期保存には向きません。に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
- **キャッシュは大容量と高速性を両立しにくい**。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズ大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセス遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスす。
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png)
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png)
!!! tip
コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。
コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。
全体的に、**ハードディスクは大量データの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータ命令保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。
要するに、**ハードディスクは大量データの長期保存、メモリはプログラム実行中に処理しているデータの一時保存、キャッシュは頻繁にアクセスされるデータ命令保存に用いられ**、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの効率な運用を支えています。
図に示すように、プログラム実行中、データハードディスクからメモリ読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。
次の図に示すように、プログラム実行時にはデータハードディスクからメモリ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、**メモリからデータを賢く読み込むことで**、CPU高速なデータ読み出しを提供し、プログラム実行効率を大きく高め、低速なメモリへの依存を減らします。
![ハードディスク、メモリ、キャッシュ間のデータフロー](ram_and_cache.assets/computer_storage_devices.png)
![ハードディスク、メモリ、キャッシュ間のデータの流れ](ram_and_cache.assets/computer_storage_devices.png)
## データ構造のメモリ効率
メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制があります。
メモリ空間利用という観点では、配列と連結リストにはそれぞれ利点と制があります。
一方で、**メモリは限られており、複数のプログラムで共有できない**ため、データ構造での空間使用の最適化は重要です。配列要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストはノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。
一方で、**メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません**。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストはノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。
方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リスト要素を非連続の場所に保存し、頻繁な挿入削除はメモリ断片化を悪化させる可能性があります。
方で、プログラム実行中には、**メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み**、メモリ利用効率低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リスト要素は分散して格納されるため、頻繁な挿入削除を行うと、より断片化を招きやすくなります。
## データ構造のキャッシュ効率
キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、<u>キャッシュミス</u>が発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります
キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュに存在しない場合、<u>キャッシュミス(cache miss</u>が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません
明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合<u>キャッシュヒット率</u>と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。
当然ながら、**キャッシュミスが少ないほど、CPU のデータ読み書き効率高くなり**、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合<u>キャッシュヒット率cache hit rate</u>と呼び、この指標は通常、キャッシュ効率の評価に用いられます。
より高い効率を達成するため、キャッシュは以下のデータロードメカニズムを採用します。
できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。
- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。
- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。
- **空間的局所性**特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するため、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。
- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用し、最近アクセスされたデータを保持してヒット率を向上させます。
- **キャッシュライン**:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
- **プリフェッチ機構**:プロセッサはデータアクセスパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、のパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
- **空間的局所性**あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
- **時間的局所性**あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスたデータを保持することでヒット率を高めます。
実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。
実際には、**配列と連結リストはキャッシュ利用効率が異なり**、主に次の点に表れます。
- **占有空間**:連結リスト要素は配列要素より多くの空間を占有するため、キャッシュに保持される有効データ少なくなります。
- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。
- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。
- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くデータまもなくアクセスされる可能性が高くなります。
- **使用空間**:連結リスト要素は配列要素より多くの空間を占るため、キャッシュに収まる有効データ量は少なくなります。
- **キャッシュライン**:連結リストデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
- **プリフェッチ機構**:配列のほうが連結リストよりもデータアクセスパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
- **空間的局所性**:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。
全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。
全体として、**配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています**。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。
**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。
注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。
- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。
- データ量が非常に大きく、高度に動的で、スタックの想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リスト大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。
- アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
- データ量が非常に大きく、動的性が高く、スタックの想サイズを見積もりにくい場合、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
+49 -44
View File
@@ -1,81 +1,86 @@
# まとめ
### 重要な復習
### 要点の振り返り
- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています:連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います
- 配列はランダムアクセスをサポートし、使用するメモリ少ない一方で、要素の挿入と削除は非効率的で、初期化後長さが固定されています
- 連結リストは参照(ポインタ)変更によって効率的なノードの挿入と削除を実装し、長さ柔軟に調整できますが、ノードアクセス効率低く、より多くのメモリを消費します
- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります
- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします
- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります
- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です
- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます
- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。
- 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である
- 配列はランダムアクセスをサポートし、使用メモリ少ない一方で、要素の挿入と削除の効率は低く、初期化後長さを変更できない
- 連結リストは参照(ポインタ)変更することでノードの挿入と削除を効率的に行え、長さ柔軟に調整できる。一方で、ノードへのアクセス効率低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある
- リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる
- リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある
- プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である
- キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる
- 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである
### Q & A
**Q**:配列をスタックに格納するヒープに格納するは、時間と空間効率に影響ますか?
**Q**:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか?
スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます
スタックとヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作効率は本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じ
1. 割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般に利用可能メモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています
3. 柔軟性:スタック上の配列サイズはコンパイル時に決定される必要がありますが、ヒープ上の配列サイズは実行時に動的に決定できます
1. 確保と解放効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コードで動的に確保できる反面、断片化しやすい。そのため、ヒープでの確保と解放は通常スタックより遅い
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適してい
3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があが、ヒープ上の配列サイズは実行時に動的に決定でき
**Q**:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか?
**Q**:なぜ配列は同じ型の要素が求められるのに、連結リストは同じ型であることが強調されないのですか?
連結リストは参照(ポインタ)によって接続されたノードで構成され、各ノードはint、doublestringobjectなど、異なる型のデータを格納できます
連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには `int``double``string``object` など、異なる型のデータを格納でき
対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません
これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に `int``long` の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占る場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない
```shell
# 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス
# 要素メモリアドレス = 配列メモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス
```
**Q**:ノードを削除した後、`P.next``None`に設定する必要ありますか?
**Q**:ノード `P` を削除した後、`P.next``None` に設定する必要ありますか?
`P.next`を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で`P`に遭遇することはもうありません。これは、ノード`P`がリストから効果的に削除されたことを意味し、`P`が指す場所はもはやリストに影響しません
`P.next` を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや `P` に出会うことはない。つまり、ノード `P` はすでに連結リストから削除されており、この時点で `P` がどこを指していても、この連結リストに影響しない
ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ノード`P`が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、`P.next`の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります
データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある
**Q**:連結リストでは、挿入と削除操作の時間計算量は`O(1)`です。しかし、挿入や削除前の要素検索には`O(n)`時間がかかるので、なぜ時間計算量は`O(n)`ではないのですか?
**Q**:連結リストで挿入と削除の時間計算量は $O(1)$ です。しかし、追加や削除の前には要素を探すのに $O(n)$ の時間が必要です。では、なぜ時間計算量は $O(n)$ ではないのですか?
要素を最初に検索してから削除する場合、時間計算量は確かに`O(n)`です。しかし、連結リストの挿入と削除における`O(1)`の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持、各挿入削除操作`O(1)`にします
要素を先に探してから削除するのであれば、時間計算量が $O(n)$ であるのは確かである。しかし、連結リストの $O(1)$ での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入削除操作はどれも $O(1)$ になる
**Q**:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか?
**Q**「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?
図は単なる定性的な表現であり、定量的分析は特定の状況に依存します
この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある
- 異なる型のノード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです
- ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトで
- ノード値が占める領域は型によって異なり、たとえば `int``long``double`、インスタンスオブジェクトなどがある
- ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである
**Q**:リストの末尾への要素追加は常に`O(1)`ですか?
**Q**:リストの末尾への要素追加は常に $O(1)$ ですか?
要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は`O(n)`になります
要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要があ。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は $O(n)$ になる
**Q**:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間無駄につながる可能性があります」というは、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか?
**Q**:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間無駄にる可能性があ」というは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?
ここで空間の無駄は主に2つの側面を指します:一方で、リストは初期長設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常$\times 1.5$などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常完全に埋めることできません
ここでいう空間の無駄は主に 2 つの意味がある。一方で、リストは初期長設定されるが、必ずしもそれだけ必要とは限らない。もう一方で、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば $\times 1.5$ を掛ける。このため、多くの空きスロットが生、通常それらを完全に埋めることできない
**Q**Python`n = [1, 2, 3]`を初期化した後、これら3つの要素のアドレスは連続していますが、`m = [2, 1, 3]`を初期化すると、各要素の`id`は連続していないが`n`のものと同一です。これらの要素のアドレスが連続していない場合`m`はまだ配列ですか?
**Q**Python`n = [1, 2, 3]` を初期化した後、この 3 つの要素のアドレスは連続しています。しかし `m = [2, 1, 3]` を初期化すると、各要素の id は連続しておらず、それぞれ `n` 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら`m` も配列なのですか?
リスト要素を連結リストノード`n = [n1, n2, n3, n4, n5]`に置き換える場合、これら5つのノードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、`O(1)`時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです
仮にリスト要素を連結リストノード `n = [n1, n2, n3, n4, n5]` に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として $O(1)$ 時間でノードのメモリアドレスを取得し、対応するノードにアクセスでき。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである
多くの言語と異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同`id`を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります
多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。
**Q**C++ STL`std::list`はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制がありますか?
**Q**C++ STL`std::list` はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制があるのでしょうか?
一方で、アルゴリズム実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります
一方では、私たちは多くの場合、アルゴリズム実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある
- 空間オーバーヘッド:各要素に2つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list`は通常`std::vector`より多くの空間を占有します
- キャッシュ非友好的:データが連続して格納されていないため、`std::list`はキャッシュ利用率が低くなります。一般に、`std::vector`の方がパフォーマンスが優れています
- 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list` は通常 `std::vector` より多くの空間を消費する
- キャッシュ非効率:データが連続して格納されていないため、`std::list` はキャッシュ利用率が低。一般に`std::vector` のほうが性能がよい
方で、連結リストは主に二分木とグラフに必要です。スタックキューは、連結リストではなく、プログラミング言語の`stack``queue`クラスを使用して実装されることが多いです
もう一方で、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックキューについては、連結リストではなく、たいてい言語が提供する `stack``queue` を使う
**Q**リスト`res = [0] * self.size()`を初期化すると、`res`の各要素は同じアドレスを参照しますか?
**Q**`res = [[0]] * n` という操作で 2 次元リストを生成した場合、それぞれの `[0]` は独立していますか?
いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト`res = [[0]] * self.size()`を初期化すると、同じリスト`[0]`を複数回参照することになります
独立していない。この 2 次元リストでは、すべての `[0]` は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる
**Q**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?
2 次元リスト内の各 `[0]` を独立させたい場合は、`res = [[0] for _ in range(n)]` を使って実現できる。この方式の原理は、独立した `[0]` リストオブジェクトを $n$ 個初期化していることにある。
データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。
**Q**`res = [0] * n` という操作で生成されたリストでは、それぞれの整数 0 は独立していますか?
このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。
それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。
しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。