Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,19 +1,19 @@
|
||||
# 配列
|
||||
|
||||
<u>配列(array)</u>は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素の<u>インデックス(index)</u>と呼びます。下図は、配列の主要な概念と格納方式を示しています。
|
||||
<u>配列(array)</u>は、同じ型の要素を連続したメモリ領域に格納する線形データ構造です。配列内での要素の位置を、その要素の<u>インデックス(index)</u>と呼びます。下図に、配列の主な概念と格納方法を示します。
|
||||
|
||||

|
||||

|
||||
|
||||
## 配列の一般的な操作
|
||||
## 配列の主な操作
|
||||
|
||||
### 配列の初期化
|
||||
|
||||
必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は $0$ に初期化されます。
|
||||
配列の初期化には、初期値を指定しない方法と、初期値を指定する方法があり、必要に応じて選べます。初期値を指定しない場合、多くのプログラミング言語では要素が $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)を配列(Array)として扱う
|
||||
// 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 の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない
|
||||
// Vector は Rust で一般に動的配列として使われる型
|
||||
// 拡張 extend() メソッドを実装しやすくするため、以下では vector を配列(array)として扱う
|
||||
// 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,7 +113,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="array.kt"
|
||||
/* 配列を初期化する */
|
||||
/* 配列を初期化 */
|
||||
var arr = IntArray(5) // { 0, 0, 0, 0, 0 }
|
||||
var nums = intArrayOf(1, 3, 2, 5, 4)
|
||||
```
|
||||
@@ -121,24 +121,24 @@
|
||||
=== "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
|
||||
https://pythontutor.com/render.html#code=%23%20%E9%85%8D%E5%88%97%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### 要素へのアクセス
|
||||
|
||||
配列要素は連続したメモリ領域に格納されるため、要素のメモリアドレスの計算は非常に容易です。配列のメモリアドレス(先頭要素のメモリアドレス)とある要素のインデックスが与えられれば、下図の式を使ってその要素のメモリアドレスを計算でき、直接その要素にアクセスできます。
|
||||
配列の要素は連続したメモリ領域に格納されるため、各要素のメモリアドレスは簡単に計算できます。配列のメモリアドレス(先頭要素のメモリアドレス)と要素のインデックスがわかれば、下図の式でその要素のメモリアドレスを求め、直接アクセスできます。
|
||||
|
||||

|
||||
|
||||
上図を見ると、配列の最初の要素のインデックスは $0$ であり、これは少し直感に反するように思えます。というのも、$1$ から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは $0$ であるため、そのインデックスが $0$ なのは妥当です。
|
||||
上図では、配列の先頭要素のインデックスが $0$ になっています。数えるなら $1$ から始める方が自然なので、少し直感に反するかもしれません。しかし、アドレスの計算式から見ると、**インデックスは本質的にメモリアドレスのオフセットを表します**。先頭要素のアドレスのオフセットは $0$ なので、インデックスが $0$ であるのは理にかなっています。
|
||||
|
||||
配列では要素へのアクセスは非常に効率的であり、$O(1)$ 時間で任意の要素にランダムアクセスできます。
|
||||
配列の要素へのアクセスは非常に効率的で、任意の要素に $O(1)$ 時間でランダムアクセスできます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{random_access}
|
||||
@@ -146,11 +146,11 @@
|
||||
|
||||
### 要素の挿入
|
||||
|
||||
配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。
|
||||
配列の要素はメモリ内で「隙間なく並んで」おり、その間にほかのデータを入れる余地はありません。下図のように、配列の途中に要素を挿入するには、その位置以降の要素をすべて一つ後ろへずらしてから、そのインデックスに要素を代入します。
|
||||
|
||||

|
||||

|
||||
|
||||
注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。
|
||||
配列の長さは固定されているため、要素を挿入すると、末尾の要素が必ず「失われる」ことに注意してください。この問題の解決方法は「リスト」の節で説明します。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{insert}
|
||||
@@ -158,70 +158,70 @@
|
||||
|
||||
### 要素の削除
|
||||
|
||||
同様に、下図のように、インデックス $i$ の要素を削除したい場合は、インデックス $i$ より後ろの要素をすべて 1 つずつ前へずらす必要があります。
|
||||
同様に、下図のようにインデックス $i$ の要素を削除するには、インデックス $i$ より後ろにある要素をすべて一つ前へずらします。
|
||||
|
||||

|
||||

|
||||
|
||||
注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。
|
||||
削除後、元の末尾の要素は「意味を持たなくなる」ので、わざわざ変更する必要はありません。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
全体として見ると、配列の挿入と削除には次の欠点があります。
|
||||
配列への挿入と削除には、全体として次の欠点があります。
|
||||
|
||||
- **時間計算量が高い**:配列の挿入と削除の平均時間計算量はいずれも $O(n)$ であり、ここで $n$ は配列長です。
|
||||
- **要素が失われる**:配列の長さは不変であるため、要素を挿入すると配列長の範囲を超えた要素は失われます。
|
||||
- **メモリの浪費**:やや長めの配列を初期化して先頭部分だけを使うこともでき、この場合データ挿入時に失われる末尾要素はすべて「無意味」ですが、その代わり一部のメモリ領域が無駄になります。
|
||||
- **時間計算量が大きい**:配列への挿入と削除の平均時間計算量は、どちらも $O(n)$ です。ここで $n$ は配列の長さです。
|
||||
- **要素が失われる**:配列の長さは変更できないため、要素を挿入すると、配列の長さを超えた要素は失われます。
|
||||
- **メモリが無駄になる**:長めの配列を初期化し、前半だけを使えば、挿入時に失われる末尾の要素を「意味のない」ものにできます。ただし、この方法ではメモリ領域の一部が無駄になります。
|
||||
|
||||
### 配列の走査
|
||||
|
||||
ほとんどのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます。
|
||||
多くのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
### 要素の検索
|
||||
### 要素の探索
|
||||
|
||||
配列内で指定した要素を探すには、配列を走査し、各反復で要素値が一致するかを判定し、一致したら対応するインデックスを出力します。
|
||||
配列内で指定した要素を探すには、配列を走査し、各要素の値が一致するかを調べます。一致したら、そのインデックスを返します。
|
||||
|
||||
配列は線形データ構造であるため、上記の検索操作は「線形探索」と呼ばれます。
|
||||
配列は線形データ構造であるため、この探索を「線形探索」と呼びます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
### 配列の拡張
|
||||
### 配列の容量拡張
|
||||
|
||||
複雑なシステム環境では、配列の後方にあるメモリ領域が利用可能であることをプログラム側で保証できず、そのため安全に配列容量を拡張できません。したがって、ほとんどのプログラミング言語では、**配列の長さは不変です**。
|
||||
複雑なシステム環境では、配列の直後のメモリ領域が空いているとは限らないため、安全に容量を増やすことができません。そのため、多くのプログラミング言語では、**配列の長さは変更できません**。
|
||||
|
||||
配列を拡張したい場合は、より大きな新しい配列を作り、元の配列の要素を順に新配列へコピーする必要があります。これは $O(n)$ の操作であり、配列が大きい場合は非常に時間がかかります。コードは次のとおりです。
|
||||
配列の容量を増やしたい場合は、より大きな配列を新しく作り、元の配列の要素を順にコピーする必要があります。これは $O(n)$ の操作であり、配列が大きいと非常に時間がかかります。コードを次に示します。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
## 配列の利点と限界
|
||||
## 配列の利点と制約
|
||||
|
||||
配列は連続したメモリ領域に格納され、要素の型も同一です。この方法には豊富な事前情報が含まれており、システムはそれらを利用してデータ構造の操作効率を最適化できます。
|
||||
配列は連続したメモリ領域に格納され、要素の型も同じです。この性質には豊富な事前情報が含まれており、システムはそれを利用して操作の効率を高められます。
|
||||
|
||||
- **空間効率が高い**:配列はデータに連続したメモリブロックを割り当てるため、追加の構造オーバーヘッドが不要です。
|
||||
- **ランダムアクセスをサポートする**:配列では任意の要素に $O(1)$ 時間でアクセスできます。
|
||||
- **キャッシュ局所性**:配列要素にアクセスする際、コンピュータはその要素だけでなく周囲のデータもキャッシュするため、高速キャッシュを利用して後続操作の実行速度を高められます。
|
||||
- **空間効率が高い**:データに連続したメモリ領域を割り当てるため、構造を管理する追加のコストが不要です。
|
||||
- **ランダムアクセスに対応する**:任意の要素に $O(1)$ 時間でアクセスできます。
|
||||
- **キャッシュの局所性を活かせる**:配列の要素にアクセスすると、コンピュータはその要素だけでなく周辺のデータもキャッシュに読み込みます。高速なキャッシュにより、その後の操作が速くなります。
|
||||
|
||||
連続領域への格納は諸刃の剣であり、次のような制約があります。
|
||||
連続した領域への格納には、利点だけでなく、次の制約もあります。
|
||||
|
||||
- **挿入と削除の効率が低い**:配列内の要素が多い場合、挿入や削除では大量の要素を移動する必要があります。
|
||||
- **長さが不変**:配列は初期化後に長さが固定され、拡張するにはすべてのデータを新しい配列へコピーする必要があり、コストが大きくなります。
|
||||
- **空間の浪費**:配列に割り当てたサイズが実際の必要量を上回る場合、余分な領域は無駄になります。
|
||||
- **挿入と削除の効率が低い**:要素が多い場合、挿入や削除の際に大量の要素を移動する必要があります。
|
||||
- **長さを変更できない**:配列の長さは初期化後に固定されます。容量を増やすには全データを新しい配列へコピーする必要があり、大きなコストがかかります。
|
||||
- **空間が無駄になる**:実際に必要な量より大きな配列を確保すると、余った空間は無駄になります。
|
||||
|
||||
## 配列の典型的な応用
|
||||
## 配列の代表的な用途
|
||||
|
||||
配列は基礎的で一般的なデータ構造であり、さまざまなアルゴリズムで頻繁に使われるだけでなく、多様な複雑データ構造の実装にも利用できます。
|
||||
配列は基本的で広く使われるデータ構造です。各種アルゴリズムに頻繁に使われるほか、複雑なデータ構造の実装にも使えます。
|
||||
|
||||
- **ランダムアクセス**:いくつかのサンプルをランダムに抽出したい場合、配列に格納してランダムな系列を生成し、インデックスに基づいてランダムサンプリングを行えます。
|
||||
- **ソートと探索**:配列はソートアルゴリズムと探索アルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは主に配列上で行われます。
|
||||
- **ルックアップテーブル**:ある要素やその対応関係を高速に調べる必要がある場合、配列をルックアップテーブルとして使えます。たとえば文字から ASCII コードへの対応を実装したいなら、文字の ASCII コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
|
||||
- **機械学習**:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
|
||||
- **データ構造の実装**:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
|
||||
- **ランダムアクセス**:無作為にサンプルを抽出したい場合は、データを配列に格納し、乱数列を生成して、その値をインデックスに使えばランダムサンプリングができます。
|
||||
- **ソートと探索**:配列はソートや探索のアルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは、主に配列上で行います。
|
||||
- **参照表**:要素やその対応関係をすばやく調べたい場合、配列を参照表として使えます。たとえば文字から ASCII コードへの対応を実装するには、文字の ASCII コード値をインデックスとし、配列の対応する位置に要素を格納します。
|
||||
- **機械学習**:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算を大量に使い、それらのデータは配列として構成されます。配列はニューラルネットワークのプログラミングで最もよく使われるデータ構造です。
|
||||
- **データ構造の実装**:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどの実装に使えます。たとえば、グラフの隣接行列は実際には二次元配列です。
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
# 演習
|
||||
# 練習問題
|
||||
|
||||
## 確認問題
|
||||
## 知識の確認
|
||||
|
||||
### 配列と連結リストが要素を見つける方法
|
||||
### 配列と連結リストで要素を見つけるには
|
||||
|
||||
配列と単方向連結リストに、どちらも `[A, B, C, D, E]` が順に保存されています。ここで 4 番目の要素 `D` を読み取ります。
|
||||
配列と単方向連結リストに、どちらも `[A, B, C, D, E]` が順に保存されています。4 番目の要素 `D` を読み取ります。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 配列では、どのインデックスを直接使えますか?
|
||||
2. 単方向連結リストでは、先頭ノード `A` から始めて、`next` をたどりながらどのノードを通りますか?
|
||||
3. 読み取る位置が後ろになるほど、2 つの構造で必要な手順はどのように変わりますか?位置を指定した読み取りを繰り返すには、どちらが適していますか?その理由も説明してください。
|
||||
1. 配列では、どのインデックスを直接使えますか。
|
||||
2. 単方向連結リストでは、先頭ノード `A` から `next` をたどると、どのノードを順に通りますか。
|
||||
3. 読み取る要素の位置が後ろになるにつれ、各構造で必要な手順の数はどう変わりますか。位置を指定した読み取りを繰り返す用途にはどちらが適していますか。理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. インデックスが 0 から始まる場合、4 番目の要素のインデックスは 3 なので、配列では `arr[3]` を直接読み取れます。
|
||||
1. インデックスが 0 から始まるなら、4 番目の要素のインデックスは 3 なので、配列では `arr[3]` を直接読み取れます。
|
||||
|
||||
2. 単方向連結リストでは、必ず先頭から始めます。アクセスする経路は `A → B → C → D` で、`next` を 3 回たどります。
|
||||
2. 単方向連結リストは先頭からたどる必要があります。訪問経路は `A → B → C → D` で、`next` に沿って 3 回進みます。
|
||||
|
||||
3. 配列は先頭アドレスとインデックスから要素の位置を直接求められるため、位置を指定したアクセスの時間計算量は $O(1)$ です。
|
||||
単方向連結リストで $k$ 番目のノードにアクセスするには、先頭ノードから `next` を $k-1$ 回たどる必要があり、
|
||||
最悪の場合は $O(n)$ の時間がかかります。
|
||||
単方向連結リストの $k$ 番目のノードにアクセスするには、先頭ノードから `next` に沿って $k-1$ 回進む必要があり、
|
||||
最悪の場合は $O(n)$ 時間かかります。
|
||||
|
||||
ここでは位置を指定した読み取りだけを比較しており、連結リストがすべての操作で遅いという意味ではありません。
|
||||
ここで比較しているのは位置を指定した読み取りだけであり、連結リストがすべての操作で遅いという意味ではありません。
|
||||
|
||||
### 配列と連結リストに要素を挿入する方法
|
||||
### 配列と連結リストに要素を挿入するには
|
||||
|
||||
配列と単方向連結リストに、どちらも `A、B、C、D` が保存されています。ここで `B` の後ろに `X` を挿入します。
|
||||
配列と単方向連結リストに、どちらも `A、B、C、D` が保存されています。`B` の後ろに `X` を挿入します。
|
||||
|
||||
- 配列の容量は 5 で、現在の状態は `[A, B, C, D, _]` です。
|
||||
- 連結リストは `A → B → C → D` で、ノード `B` を指す参照はすでに得られています。
|
||||
- 連結リストは `A → B → C → D` で、ノード `B` を指す参照をすでに取得しています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 配列では、どの要素を移動する必要がありますか?挿入後の配列も書いてください。
|
||||
2. 連結リストでは、`X.next` と `B.next` をどの順序で変更すべきですか?挿入後の連結リストも書いてください。
|
||||
3. 挿入の効率を比較するときに、「ノード B を指す参照はすでに得られている」と明記する必要があるのはなぜですか?
|
||||
1. 配列では、どの要素を移動する必要がありますか。挿入後の配列を書いてください。
|
||||
2. 連結リストでは、`X.next` と `B.next` をどの順番で変更しますか。挿入後の連結リストを書いてください。
|
||||
3. 挿入の効率を比較するとき、「ノード B の参照をすでに取得している」と明記するのはなぜですか。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. 配列では、まず `D` を 1 つ右へ移動し、次に `C` を 1 つ右へ移動します。最後に `X` をインデックス 2 に置くと、
|
||||
`[A, B, X, C, D]` になります。
|
||||
1. 配列では、まず `D` を右へ一つ、次に `C` を右へ一つ移動し、最後に `X` をインデックス 2 に置きます。
|
||||
結果は `[A, B, X, C, D]` です。
|
||||
|
||||
2. `B.next` はもともと `C` を指しています。まず `X.next = B.next` として `X` が `C` を指すようにし、
|
||||
次に `B.next = X` とします。その結果、`A → B → X → C → D` となります。
|
||||
2. 元の `B.next` は `C` を指しています。まず `X.next = B.next` として `X` を `C` につなぎ、
|
||||
次に `B.next = X` とします。結果は `A → B → X → C → D` です。
|
||||
元の接続を保存せずに先に `B.next` を上書きすると、`C` を見つけられなくなる可能性があります。
|
||||
|
||||
3. `B` の位置が分かっていれば、連結リストへの挿入は 2 つの接続を変更するだけなので、$O(1)$ の時間で行えます。
|
||||
先頭から `B` を探す必要もある場合、その探索自体に $O(n)$ の時間がかかる可能性があります。
|
||||
3. `B` の位置が分かっていれば、連結リストへの挿入は二つの接続を変更するだけなので、$O(1)$ 時間で完了します。
|
||||
先頭から `B` を探す必要があれば、その探索自体に $O(n)$ 時間かかる場合があります。
|
||||
|
||||
### リストの容量が増える仕組み
|
||||
|
||||
配列を基に実装されたリストの現在の内容が `[A, B, C]`、長さが `size = 3`、容量が `capacity = 4` であるとします。
|
||||
容量が足りないときは、新しい配列の容量を元の 2 倍にするものとします。
|
||||
配列で実装したリストの現在の内容は `[A, B, C]`、長さは `size = 3`、容量は `capacity = 4` です。
|
||||
容量が足りなくなったら、新しい配列の容量を元の 2 倍にするものとします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `D` を追加した後、リストの長さと容量はそれぞれいくつですか?容量を増やす必要はありますか?
|
||||
2. 続けて `E` を追加すると、容量はいくつになりますか?既存の要素をいくつコピーする必要がありますか?
|
||||
3. 内部で使う配列の長さは変えられないのに、リストの容量が増えたように見えるのはなぜですか?
|
||||
1. `D` を追加すると、リストの長さと容量はそれぞれいくつになりますか。容量の拡張は必要ですか。
|
||||
2. 続いて `E` を追加するとき、容量はいくつになりますか。元の要素を何個コピーする必要がありますか。
|
||||
3. 内部の配列の長さは変えられないのに、リストの容量が増えるように見えるのはなぜですか。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. `D` は最後の空き位置に置けます。このとき内容は `[A, B, C, D]`、
|
||||
`size = 4`、`capacity = 4` となり、容量を増やす必要はありません。
|
||||
1. `D` は最後の空き位置に置けます。内容は `[A, B, C, D]`、
|
||||
`size = 4`、`capacity = 4` となり、容量の拡張は不要です。
|
||||
|
||||
2. さらに `E` を追加するときは空き位置がないため、容量 8 の新しい配列を作り、
|
||||
既存の要素 `A、B、C、D` の 4 つをコピーしてから `E` を追加します。
|
||||
このとき `size = 5`、`capacity = 8` です。
|
||||
2. さらに `E` を追加するときには空きがないため、容量 8 の新しい配列を作り、
|
||||
元の `A、B、C、D` の計 4 要素をコピーしてから `E` を追加します。
|
||||
このとき、`size = 5`、`capacity = 8` となります。
|
||||
|
||||
3. 元の配列そのものが長くなったわけではありません。リストはより大きな新しい配列を作って既存の要素をコピーし、
|
||||
内部の保存先を新しい配列に切り替えます。そのため、利用者から見ると容量が増えたように見えます。
|
||||
3. 元の配列自体が長くなったわけではありません。リストがより大きな配列を新しく作り、元の要素をコピーし、
|
||||
内部の格納先を新しい配列に切り替えるため、利用者からは容量が増えたように見えます。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 配列で表した大きな整数に 1 を足す
|
||||
|
||||
配列 `digits` には、0 以上の整数の各桁が左から右へ保存されています。たとえば、`[3, 0, 8]` は 308 を表します。
|
||||
数値 0 は `[0]` で表し、それ以外の入力の先頭の桁は 0 ではありません。
|
||||
配列 `digits` には、非負整数の各桁が左から右へ保存されています。例えば `[3, 0, 8]` は 308 を表します。
|
||||
0 は `[0]` で表し、それ以外の入力では先頭の桁は 0 ではありません。
|
||||
|
||||
十進数の筆算と同じように、この整数に 1 を足し、結果を同じ配列形式で返してください。
|
||||
`digits` は直接変更してかまいません。先頭に新しい繰り上がりが生じた場合は、より長い配列を返せます。
|
||||
十進数の筆算を模して、この整数に 1 を足し、結果を同じ配列の形式で返してください。
|
||||
`digits` を直接変更して構いません。先頭に新しい繰り上がりが生じたら、より長い配列を返せます。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
??? tip "解くためのヒント"
|
||||
|
||||
1. 筆算の加算と同じように、配列の末尾の桁から始めます
|
||||
2. 現在の桁が 9 より小さければ、1 を足してすぐに返せます
|
||||
3. 現在の桁が 9 なら 0 に変えます。すべての桁が 9 だった場合は、先頭に新しく 1 を置く必要があります
|
||||
1. 筆算と同じように、配列の最後の桁から始めます
|
||||
2. 現在の桁が 9 未満なら、1 を足してすぐに返せます
|
||||
3. 現在の桁が 9 なら 0 に変えます。すべての桁が 9 なら、先頭に 1 を補う必要があります
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 単方向連結リストの反転
|
||||
|
||||
単方向連結リストの先頭ノード `head` が与えられます。各ノードには、値と次のノードを指す `next` が含まれています。
|
||||
単方向連結リストの先頭ノード `head` が与えられます。各ノードは、値と次のノードを指す `next` を持ちます。
|
||||
|
||||
反復を使ってすべてのノード間の接続を反転し、反転後の先頭ノードを返してください。
|
||||
新しい連結リストノードを作ってはいけません。
|
||||
反復によってノード間のすべての接続を反転し、反転後の先頭ノードを返してください。
|
||||
新しい連結リストのノードは作らないでください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
??? tip "解くためのヒント"
|
||||
|
||||
1. まず紙に 3 つの連続したノードと、2 つのポインタ prev、cur を描きます
|
||||
2. cur.next を書き換える前に、元の次のノードを nxt に保存する必要があります
|
||||
3. cur.next を反転した後、prev = cur、続いて cur = nxt とし、元の連結リストの次のノードを処理します
|
||||
1. まず紙に、つながった三つのノードと二つのポインタ prev、cur を描いてみましょう
|
||||
2. cur.next を書き換える前に、元の次のノードを必ず nxt に保存します
|
||||
3. cur.next を反転したら、prev = cur、続いて cur = nxt として、元の連結リストの次のノードを処理します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
!!! abstract
|
||||
|
||||
データ構造の世界は、まるで重厚なれんがの壁のようです。
|
||||
データ構造の世界は、分厚いレンガの壁のようです。
|
||||
|
||||
配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。
|
||||
配列のレンガは整然と並び、互いにぴったり接しています。連結リストのレンガはあちこちに散らばり、それらを結ぶつるがレンガの隙間を自由に行き交います。
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,26 +1,26 @@
|
||||
# 連結リスト
|
||||
|
||||
メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。
|
||||
メモリ空間はすべてのプログラムが利用する共通の資源です。複雑なシステムの実行環境では、空き領域がメモリの各所に散らばっていることがあります。配列には連続したメモリ領域が必要ですが、配列が非常に大きいと、それだけの連続領域を確保できないかもしれません。このようなとき、連結リストの柔軟性が活きてきます。
|
||||
|
||||
<u>連結リスト(linked list)</u>は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。
|
||||
<u>連結リスト(linked list)</u>は、各要素がノードオブジェクトであり、ノード同士が「参照」でつながる線形データ構造です。参照には次のノードのメモリアドレスが記録されており、それを使って現在のノードから次のノードへアクセスできます。
|
||||
|
||||
連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。
|
||||
この設計により、各ノードをメモリの各所に分散して格納でき、メモリアドレスが連続している必要はありません。
|
||||
|
||||

|
||||

|
||||
|
||||
上図を見ると、連結リストの構成単位は<u>ノード(node)</u>オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。
|
||||
上図のように、連結リストを構成する単位は<u>ノード(node)</u>オブジェクトです。各ノードは、ノードの「値」と次のノードを指す「参照」という二つのデータを持ちます。
|
||||
|
||||
- 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
|
||||
- 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ `null`、`nullptr`、`None` と表記します。
|
||||
- C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。
|
||||
- 最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
|
||||
- 末尾ノードは「空」を指します。これは Java、C++、Python でそれぞれ `null`、`nullptr`、`None` と表します。
|
||||
- C、C++、Go、Rust などのポインタをサポートする言語では、上記の「参照」は「ポインタ」と読み替えます。
|
||||
|
||||
以下のコードが示すように、連結リストノード `ListNode` は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、**連結リストは配列より多くのメモリ空間を消費します**。
|
||||
次のコードのように、連結リストのノード `ListNode` は、値に加えて参照(ポインタ)も保存する必要があります。そのため、同じデータ量なら、**連結リストは配列より多くのメモリを使います**。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""連結リストノードクラス"""
|
||||
"""連結リストのノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノードの値
|
||||
self.next: ListNode | None = None # 次のノードへの参照
|
||||
@@ -29,7 +29,7 @@
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 連結リストノード構造体 */
|
||||
/* 連結リストのノード構造体 */
|
||||
struct ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode *next; // 次のノードへのポインタ
|
||||
@@ -40,7 +40,7 @@
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode next; // 次のノードへの参照
|
||||
@@ -51,7 +51,7 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode(int x) { //コンストラクタ
|
||||
int val = x; // ノードの値
|
||||
ListNode? next; // 次のノードへの参照
|
||||
@@ -61,13 +61,13 @@
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 連結リストノード構造体 */
|
||||
/* 連結リストのノード構造体 */
|
||||
type ListNode struct {
|
||||
Val int // ノードの値
|
||||
Next *ListNode // 次のノードへのポインタ
|
||||
}
|
||||
|
||||
// NewListNode コンストラクタ。新しい連結リストを作成する
|
||||
// NewListNode コンストラクタで、新しい連結リストを作る
|
||||
func NewListNode(val int) *ListNode {
|
||||
return &ListNode{
|
||||
Val: val,
|
||||
@@ -79,7 +79,7 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode {
|
||||
var val: Int // ノードの値
|
||||
var next: ListNode? // 次のノードへの参照
|
||||
@@ -93,7 +93,7 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode {
|
||||
constructor(val, next) {
|
||||
this.val = (val === undefined ? 0 : val); // ノードの値
|
||||
@@ -105,7 +105,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
@@ -119,7 +119,7 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode? next; // 次のノードへの参照
|
||||
@@ -132,7 +132,7 @@
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // ノードの値
|
||||
@@ -143,7 +143,7 @@
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 連結リストノード構造体 */
|
||||
/* 連結リストのノード構造体 */
|
||||
typedef struct ListNode {
|
||||
int val; // ノードの値
|
||||
struct ListNode *next; // 次のノードへのポインタ
|
||||
@@ -162,7 +162,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* 連結リストノードクラス */
|
||||
/* 連結リストのノードクラス */
|
||||
// コンストラクタ
|
||||
class ListNode(x: Int) {
|
||||
val _val: Int = x // ノードの値
|
||||
@@ -173,7 +173,7 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
# 連結リストノードクラス
|
||||
# 連結リストのノードクラス
|
||||
class ListNode
|
||||
attr_accessor :val # ノードの値
|
||||
attr_accessor :next # 次のノードへの参照
|
||||
@@ -185,11 +185,11 @@
|
||||
end
|
||||
```
|
||||
|
||||
## 連結リストの基本操作
|
||||
## 連結リストの主な操作
|
||||
|
||||
### 連結リストの初期化
|
||||
|
||||
連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で `next` をたどってすべてのノードに順にアクセスできます。
|
||||
連結リストは二つの手順で作ります。まず各ノードオブジェクトを初期化し、次にノード間の参照関係を構築します。初期化後は、先頭ノードから `next` の参照をたどり、すべてのノードを順に訪問できます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
n2 = ListNode(2)
|
||||
n3 = ListNode(5)
|
||||
n4 = ListNode(4)
|
||||
# ノード間の参照を構築
|
||||
# ノード間の参照を設定
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
@@ -218,7 +218,7 @@
|
||||
ListNode* n2 = new ListNode(2);
|
||||
ListNode* n3 = new ListNode(5);
|
||||
ListNode* n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
@@ -235,7 +235,7 @@
|
||||
ListNode n2 = new ListNode(2);
|
||||
ListNode n3 = new ListNode(5);
|
||||
ListNode n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -252,7 +252,7 @@
|
||||
ListNode n2 = new(2);
|
||||
ListNode n3 = new(5);
|
||||
ListNode n4 = new(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -269,7 +269,7 @@
|
||||
n2 := NewListNode(2)
|
||||
n3 := NewListNode(5)
|
||||
n4 := NewListNode(4)
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.Next = n1
|
||||
n1.Next = n2
|
||||
n2.Next = n3
|
||||
@@ -286,7 +286,7 @@
|
||||
let n2 = ListNode(x: 2)
|
||||
let n3 = ListNode(x: 5)
|
||||
let n4 = ListNode(x: 4)
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
@@ -303,7 +303,7 @@
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -320,7 +320,7 @@
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -337,7 +337,7 @@
|
||||
ListNode n2 = ListNode(2);
|
||||
ListNode n3 = ListNode(5);
|
||||
ListNode n4 = ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -355,7 +355,7 @@
|
||||
let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));
|
||||
let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));
|
||||
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.borrow_mut().next = Some(n1.clone());
|
||||
n1.borrow_mut().next = Some(n2.clone());
|
||||
n2.borrow_mut().next = Some(n3.clone());
|
||||
@@ -372,7 +372,7 @@
|
||||
ListNode* n2 = newListNode(2);
|
||||
ListNode* n3 = newListNode(5);
|
||||
ListNode* n4 = newListNode(4);
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
@@ -389,7 +389,7 @@
|
||||
val n2 = ListNode(2)
|
||||
val n3 = ListNode(5)
|
||||
val n4 = ListNode(4)
|
||||
// ノード間の参照を構築
|
||||
// ノード間の参照を設定
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
@@ -406,26 +406,26 @@
|
||||
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 "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%80%A3%E7%B5%90%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%AF%E3%83%A9%E3%82%B9%22%22%22%0A%20%20%20%20def%20__init__(self%2C%20val%3A%20int)%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E5%80%A4%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E6%AC%A1%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E7%85%A7%0A%0A%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%80%A3%E7%B5%90%E3%83%AA%E3%82%B9%E3%83%88%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20%23%20%E5%90%84%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20n0%20%3D%20ListNode(1)%0A%20%20%20%20n1%20%3D%20ListNode(3)%0A%20%20%20%20n2%20%3D%20ListNode(2)%0A%20%20%20%20n3%20%3D%20ListNode(5)%0A%20%20%20%20n4%20%3D%20ListNode(4)%0A%20%20%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%E9%96%93%E3%81%AE%E5%8F%82%E7%85%A7%E3%82%92%E8%A8%AD%E5%AE%9A%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` と表せます。
|
||||
配列は全体で一つの変数です。たとえば配列 `nums` は、`nums[0]` や `nums[1]` などの要素を含みます。一方、連結リストは複数の独立したノードオブジェクトからなります。**通常は先頭ノードで連結リスト全体を表します**。たとえば、上のコードの連結リストは、連結リスト `n0` と呼べます。
|
||||
|
||||
### ノードの挿入
|
||||
|
||||
連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード `n0` と `n1` の間に新しいノード `P` を挿入したいとします。**このとき 2 つのノードの参照(ポインタ)を変更するだけでよく**、時間計算量は $O(1)$ です。
|
||||
連結リストへのノードの挿入は簡単です。下図のように、隣り合うノード `n0` と `n1` の間に新しいノード `P` を挿入するには、**二つのノード参照(ポインタ)を変更するだけ**で済み、時間計算量は $O(1)$ です。
|
||||
|
||||
これに対して、配列に要素を挿入する時間計算量は $O(n)$ であり、データ量が大きい場合の効率は低くなります。
|
||||
対して、配列への要素の挿入は時間計算量が $O(n)$ であり、データ量が大きいと非効率になります。
|
||||
|
||||

|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{insert}
|
||||
@@ -433,11 +433,11 @@
|
||||
|
||||
### ノードの削除
|
||||
|
||||
下図に示すように、連結リストでのノード削除も非常に簡単で、**1 つのノードの参照(ポインタ)を変更するだけで済みます**。
|
||||
下図のように、連結リストからのノードの削除も簡単で、**一つのノードの参照(ポインタ)を変更するだけ**です。
|
||||
|
||||
なお、削除操作が完了した後もノード `P` は依然として `n1` を指していますが、実際にはこの連結リストをたどっても `P` へはアクセスできません。つまり、`P` はすでにこの連結リストには属していません。
|
||||
削除後もノード `P` は `n1` を指していますが、この連結リストを走査しても `P` には到達できなくなることに注意してください。つまり、`P` はもはやこの連結リストに属していません。
|
||||
|
||||

|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{remove}
|
||||
@@ -445,7 +445,7 @@
|
||||
|
||||
### ノードへのアクセス
|
||||
|
||||
**連結リストでノードにアクセスする効率は低い**です。前節で述べたように、配列では任意の要素へ $O(1)$ 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、1 つずつ後ろへたどって目的のノードを見つける必要があります。つまり、連結リストの第 $i$ ノードにアクセスするには $i - 1$ 回のループが必要であり、時間計算量は $O(n)$ です。
|
||||
**連結リストのノードへのアクセスは効率が低くなります**。前の節で説明したように、配列では任意の要素に $O(1)$ 時間でアクセスできます。連結リストでは、先頭ノードから順にたどり、目的のノードを見つける必要があります。つまり、$i$ 番目のノードにアクセスするには $i - 1$ 回ループする必要があり、時間計算量は $O(n)$ です。
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{access}
|
||||
@@ -453,54 +453,54 @@
|
||||
|
||||
### ノードの探索
|
||||
|
||||
連結リストを走査し、その中から値が `target` のノードを探し、そのノードの連結リスト内でのインデックスを出力します。この処理も線形探索に属します。コードは次のとおりです。
|
||||
連結リストを走査して値が `target` のノードを探し、そのノードのインデックスを返します。これも線形探索です。コードを次に示します。
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## 配列 vs. 連結リスト
|
||||
## 配列と連結リストの比較
|
||||
|
||||
次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。
|
||||
下表に、配列と連結リストの特徴と操作効率をまとめます。両者は対照的な格納戦略を取るため、性質や操作効率も対照的になります。
|
||||
|
||||
<p align="center"> 表 <id> 配列と連結リストの効率比較 </p>
|
||||
<p align="center"> 表 <id> 配列と連結リストの効率の比較 </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 種類あります。
|
||||
|
||||
- **単方向連結リスト**:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 `None` を指します。
|
||||
- **循環連結リスト**:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
|
||||
- **双方向連結リスト**:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
|
||||
- **単方向連結リスト**:これまで説明した通常の連結リストです。各ノードは、値と次のノードへの参照の二つを持ちます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空の `None` を指します。
|
||||
- **循環連結リスト**:単方向連結リストの末尾ノードを先頭ノードに向け、末尾と先頭をつなぐと、循環連結リストになります。循環連結リストでは、任意のノードを先頭とみなせます。
|
||||
- **双方向連結リスト**:単方向連結リストと異なり、両方向の参照を記録します。ノードには、後続ノード(次のノード)と先行ノード(前のノード)の両方への参照(ポインタ)を持たせます。単方向連結リストより柔軟で、どちらの方向にも走査できますが、その分メモリ使用量も増えます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""双方向連結リストノードクラス"""
|
||||
"""双方向連結リストのノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノードの値
|
||||
self.next: ListNode | None = None # 後続ノードへの参照
|
||||
self.prev: ListNode | None = None # 前駆ノードへの参照
|
||||
self.prev: ListNode | None = None # 先行ノードへの参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
/* 双方向連結リストのノード構造体 */
|
||||
struct ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode *next; // 後続ノードへのポインタ
|
||||
ListNode *prev; // 前駆ノードへのポインタ
|
||||
ListNode *prev; // 先行ノードへのポインタ
|
||||
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // コンストラクタ
|
||||
};
|
||||
```
|
||||
@@ -508,11 +508,11 @@
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode next; // 後続ノードへの参照
|
||||
ListNode prev; // 前駆ノードへの参照
|
||||
ListNode prev; // 先行ノードへの参照
|
||||
ListNode(int x) { val = x; } // コンストラクタ
|
||||
}
|
||||
```
|
||||
@@ -520,25 +520,25 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode(int x) { // コンストラクタ
|
||||
int val = x; // ノードの値
|
||||
ListNode next; // 後続ノードへの参照
|
||||
ListNode prev; // 前駆ノードへの参照
|
||||
ListNode prev; // 先行ノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
/* 双方向連結リストのノード構造体 */
|
||||
type DoublyListNode struct {
|
||||
Val int // ノードの値
|
||||
Next *DoublyListNode // 後続ノードへのポインタ
|
||||
Prev *DoublyListNode // 前駆ノードへのポインタ
|
||||
Prev *DoublyListNode // 先行ノードへのポインタ
|
||||
}
|
||||
|
||||
// NewDoublyListNode の初期化
|
||||
// NewDoublyListNode で初期化
|
||||
func NewDoublyListNode(val int) *DoublyListNode {
|
||||
return &DoublyListNode{
|
||||
Val: val,
|
||||
@@ -551,11 +551,11 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode {
|
||||
var val: Int // ノードの値
|
||||
var next: ListNode? // 後続ノードへの参照
|
||||
var prev: ListNode? // 前駆ノードへの参照
|
||||
var prev: ListNode? // 先行ノードへの参照
|
||||
|
||||
init(x: Int) { // コンストラクタ
|
||||
val = x
|
||||
@@ -566,12 +566,12 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode {
|
||||
constructor(val, next, prev) {
|
||||
this.val = val === undefined ? 0 : val; // ノードの値
|
||||
this.next = next === undefined ? null : next; // 後続ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 前駆ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 先行ノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -579,7 +579,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
@@ -587,7 +587,7 @@
|
||||
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // ノードの値
|
||||
this.next = next === undefined ? null : next; // 後続ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 前駆ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 先行ノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -595,11 +595,11 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノードの値
|
||||
ListNode? next; // 後続ノードへの参照
|
||||
ListNode? prev; // 前駆ノードへの参照
|
||||
ListNode? prev; // 先行ノードへの参照
|
||||
ListNode(this.val, [this.next, this.prev]); // コンストラクタ
|
||||
}
|
||||
```
|
||||
@@ -610,12 +610,12 @@
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* 双方向連結リストノード型 */
|
||||
/* 双方向連結リストのノード型 */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // ノードの値
|
||||
next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // 前駆ノードへのポインタ
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // 先行ノードへのポインタ
|
||||
}
|
||||
|
||||
/* コンストラクタ */
|
||||
@@ -633,11 +633,11 @@
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
/* 双方向連結リストのノード構造体 */
|
||||
typedef struct ListNode {
|
||||
int val; // ノードの値
|
||||
struct ListNode *next; // 後続ノードへのポインタ
|
||||
struct ListNode *prev; // 前駆ノードへのポインタ
|
||||
struct ListNode *prev; // 先行ノードへのポインタ
|
||||
} ListNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
@@ -654,23 +654,23 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
/* 双方向連結リストのノードクラス */
|
||||
// コンストラクタ
|
||||
class ListNode(x: Int) {
|
||||
val _val: Int = x // ノードの値
|
||||
var next: ListNode? = null // 後続ノードへの参照
|
||||
var prev: ListNode? = null // 前駆ノードへの参照
|
||||
var prev: ListNode? = null // 先行ノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
# 双方向連結リストノードクラス
|
||||
# 双方向連結リストのノードクラス
|
||||
class ListNode
|
||||
attr_accessor :val # ノードの値
|
||||
attr_accessor :next # 後続ノードへの参照
|
||||
attr_accessor :prev # 前駆ノードへの参照
|
||||
attr_accessor :prev # 先行ノードへの参照
|
||||
|
||||
def initialize(val=0, next_node=nil, prev_node=nil)
|
||||
@val = val
|
||||
@@ -680,23 +680,23 @@
|
||||
end
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## 連結リストの典型的な応用
|
||||
## 連結リストの代表的な用途
|
||||
|
||||
単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。
|
||||
単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどの実装によく使われます。
|
||||
|
||||
- **スタックとキュー**:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
|
||||
- **ハッシュテーブル**:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
|
||||
- **グラフ**:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。
|
||||
- **スタックとキュー**:挿入と削除をどちらも同じ端で行うと、先入れ後出しの性質になり、スタックに対応します。一方の端で挿入し、もう一方の端で削除すると、先入れ先出しの性質になり、キューに対応します。
|
||||
- **ハッシュテーブル**:チェイン法は、ハッシュ衝突を解決する代表的な方法の一つです。衝突したすべての要素を、一つの連結リストに格納します。
|
||||
- **グラフ**:隣接リストは、グラフを表す代表的な方法です。各頂点に連結リストを対応させ、その各要素で、当該頂点につながる別の頂点を表します。
|
||||
|
||||
双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。
|
||||
双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく使われます。
|
||||
|
||||
- **高度なデータ構造**:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
|
||||
- **ブラウザ履歴**:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
|
||||
- **LRU アルゴリズム**:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。
|
||||
- **高度なデータ構造**:たとえば赤黒木や B 木では、ノードの親にアクセスする必要があります。ノードに親への参照を保存すれば実現でき、双方向連結リストと似た考え方です。
|
||||
- **ブラウザの履歴**:ユーザーが進むボタンや戻るボタンを押すと、Web ブラウザは前後に閲覧したページを知る必要があります。双方向連結リストの性質により、この操作を簡単に実現できます。
|
||||
- **LRU アルゴリズム**:キャッシュの置換(LRU)アルゴリズムでは、最後に使われてから最も時間がたったデータをすばやく見つけ、ノードを高速に追加・削除する必要があります。双方向連結リストはこの用途に適しています。
|
||||
|
||||
循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。
|
||||
循環連結リストは、OS の資源スケジューリングなど、周期的な操作が必要な場面でよく使われます。
|
||||
|
||||
- **ラウンドロビン時間片スケジューリングアルゴリズム**:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。
|
||||
- **データバッファ**:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
|
||||
- **ラウンドロビンスケジューリング**:OS でよく使われる CPU スケジューリングの方法で、一群のプロセスを順に繰り返し処理します。各プロセスにタイムスライスを割り当て、それを使い切ると CPU は次のプロセスに切り替わります。この循環的な操作は、循環連結リストで実装できます。
|
||||
- **データバッファ**:データバッファの実装にも循環連結リストを使うことがあります。たとえば音声や動画のプレイヤーでは、データストリームを複数のバッファブロックに分け、循環連結リストに格納して、途切れない再生を実現できます。
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# リスト
|
||||
|
||||
<u>リスト(list)</u>は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。
|
||||
<u>リスト(list)</u>は抽象的なデータ構造の概念で、順序を持つ要素の集合を表します。要素へのアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量の制限を気にする必要がありません。リストは連結リストや配列を使って実装できます。
|
||||
|
||||
- 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。
|
||||
- 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。
|
||||
- 連結リストは、要素の追加・削除・検索・更新をサポートし、動的に柔軟に拡張できるため、そのままリストとみなせます。
|
||||
- 配列も要素の追加・削除・検索・更新をサポートしますが、長さを変えられないため、長さに制限のあるリストとしかみなせません。
|
||||
|
||||
配列でリストを実装する場合、**長さが不変である性質によってリストの実用性が低下する**。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。
|
||||
配列でリストを実装すると、**長さを変えられない性質が実用性を下げてしまいます**。通常、保存するデータ量を事前に確定できず、適切な長さを選ぶのが難しいためです。短すぎれば必要な量を格納できず、長すぎればメモリが無駄になります。
|
||||
|
||||
この問題を解決するために、<u>動的配列(dynamic array)</u>を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。
|
||||
この問題を解決するため、<u>動的配列(dynamic array)</u>でリストを実装できます。動的配列は配列の利点を受け継ぎ、プログラムの実行中に容量を動的に拡張できます。
|
||||
|
||||
実際には、**多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている**。たとえば、Python の `list` 、Java の `ArrayList` 、C++ の `vector` 、C# の `List` などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。
|
||||
実際、**多くのプログラミング言語の標準ライブラリで提供されるリストは、動的配列で実装されています**。Python の `list`、Java の `ArrayList`、C++ の `vector`、C# の `List` などがその例です。以下では、「リスト」と「動的配列」を同じ概念として扱います。
|
||||
|
||||
## リストの基本操作
|
||||
## リストの主な操作
|
||||
|
||||
### リストの初期化
|
||||
|
||||
通常は「初期値なし」と「初期値あり」の 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));
|
||||
```
|
||||
@@ -147,13 +147,13 @@
|
||||
nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E5%80%A4%E3%81%AA%E3%81%97%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E5%80%A4%E3%81%82%E3%82%8A%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%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"
|
||||
|
||||
@@ -278,18 +278,18 @@
|
||||
nums[1] = 0 # インデックス 1 の要素を 0 に更新
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%A6%81%E7%B4%A0%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%201%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%0A%0A%20%20%20%20%23%20%E8%A6%81%E7%B4%A0%E3%82%92%E6%9B%B4%E6%96%B0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%201%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%200%20%E3%81%AB%E6%9B%B4%E6%96%B0&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"
|
||||
|
||||
```python title="list.py"
|
||||
# リストをクリア
|
||||
# リストを空にする
|
||||
nums.clear()
|
||||
|
||||
# 末尾に要素を追加
|
||||
@@ -309,7 +309,7 @@
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -329,7 +329,7 @@
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -349,7 +349,7 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.Clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -369,7 +369,7 @@
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums = nil
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -389,7 +389,7 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.removeAll()
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -409,7 +409,7 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.length = 0;
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -429,7 +429,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.length = 0;
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -449,7 +449,7 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -469,7 +469,7 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -495,7 +495,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* リストをクリア */
|
||||
/* リストを空にする */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
@@ -515,7 +515,7 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# リストをクリア
|
||||
# リストを空にする
|
||||
nums.clear
|
||||
|
||||
# 末尾に要素を追加
|
||||
@@ -532,18 +532,18 @@
|
||||
nums.delete_at(3) # インデックス 3 の要素を削除
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E5%80%A4%E3%81%82%E3%82%8A%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E7%A9%BA%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20nums.clear()%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9C%AB%E5%B0%BE%E3%81%AB%E8%A6%81%E7%B4%A0%E3%82%92%E8%BF%BD%E5%8A%A0%0A%20%20%20%20nums.append(1)%0A%20%20%20%20nums.append(3)%0A%20%20%20%20nums.append(2)%0A%20%20%20%20nums.append(5)%0A%20%20%20%20nums.append(4)%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%94%E4%B8%AD%E3%81%AB%E8%A6%81%E7%B4%A0%E3%82%92%E6%8C%BF%E5%85%A5%0A%20%20%20%20nums.insert(3%2C%206)%20%20%23%20%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%203%20%E3%81%AB%E6%95%B0%E5%80%A4%206%20%E3%82%92%E6%8C%BF%E5%85%A5%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%A6%81%E7%B4%A0%E3%82%92%E5%89%8A%E9%99%A4%0A%20%20%20%20nums.pop(3)%20%20%20%20%20%20%20%20%23%20%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%203%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E5%89%8A%E9%99%A4&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]
|
||||
@@ -556,7 +556,7 @@
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums[i];
|
||||
@@ -572,7 +572,7 @@
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums.get(i);
|
||||
@@ -587,7 +587,7 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.Count; i++) {
|
||||
count += nums[i];
|
||||
@@ -603,7 +603,7 @@
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
count := 0
|
||||
for i := 0; i < len(nums); i++ {
|
||||
count += nums[i]
|
||||
@@ -619,7 +619,7 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
var count = 0
|
||||
for i in nums.indices {
|
||||
count += nums[i]
|
||||
@@ -635,7 +635,7 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
@@ -651,7 +651,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
@@ -667,7 +667,7 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
int count = 0;
|
||||
for (var i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
@@ -683,7 +683,7 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
// インデックスでリストを走査
|
||||
// インデックスを使ってリストを走査
|
||||
let mut _count = 0;
|
||||
for i in 0..nums.len() {
|
||||
_count += nums[i];
|
||||
@@ -705,7 +705,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* インデックスでリストを走査 */
|
||||
/* インデックスを使ってリストを走査 */
|
||||
var count = 0
|
||||
for (i in nums.indices) {
|
||||
count += nums[i]
|
||||
@@ -720,7 +720,7 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# インデックスでリストを走査
|
||||
# インデックスを使ってリストを走査
|
||||
count = 0
|
||||
for i in 0...nums.length
|
||||
count += nums[i]
|
||||
@@ -733,18 +733,18 @@
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E8%B5%B0%E6%9F%BB%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range(len(nums))%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E7%9B%B4%E6%8E%A5%E8%B5%B0%E6%9F%BB%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 つのリストを連結
|
||||
# 二つのリストを連結
|
||||
nums1: list[int] = [6, 8, 7, 10, 9]
|
||||
nums += nums1 # リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -752,7 +752,7 @@
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
vector<int> nums1 = { 6, 8, 7, 10, 9 };
|
||||
// リスト nums1 を nums の後ろに連結
|
||||
nums.insert(nums.end(), nums1.begin(), nums1.end());
|
||||
@@ -761,7 +761,7 @@
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
List<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
|
||||
nums.addAll(nums1); // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -769,7 +769,7 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.AddRange(nums1); // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -777,7 +777,7 @@
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
nums1 := []int{6, 8, 7, 10, 9}
|
||||
nums = append(nums, nums1...) // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -785,7 +785,7 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
let nums1 = [6, 8, 7, 10, 9]
|
||||
nums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -793,7 +793,7 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
const nums1 = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -801,7 +801,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
const nums1: number[] = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -809,7 +809,7 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.addAll(nums1); // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -817,7 +817,7 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
let nums1: Vec<i32> = vec![6, 8, 7, 10, 9];
|
||||
nums.extend(nums1);
|
||||
```
|
||||
@@ -831,7 +831,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* 2 つのリストを連結 */
|
||||
/* 二つのリストを連結 */
|
||||
val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()
|
||||
nums.addAll(nums1) // リスト nums1 を nums の後ろに連結
|
||||
```
|
||||
@@ -839,87 +839,87 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# 2 つのリストを連結
|
||||
# 二つのリストを連結
|
||||
nums1 = [6, 8, 7, 10, 9]
|
||||
nums += nums1
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E4%BA%8C%E3%81%A4%E3%81%AE%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E9%80%A3%E7%B5%90%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%20nums1%20%E3%82%92%20nums%20%E3%81%AE%E5%BE%8C%E3%82%8D%E3%81%AB%E9%80%A3%E7%B5%90&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"
|
||||
@@ -932,29 +932,29 @@
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* リストをソート */
|
||||
nums.sort() // ソート後、リスト要素は小さい順に並ぶ
|
||||
nums.sort() // ソート後、リストの要素は昇順に並ぶ
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# リストをソート
|
||||
nums = nums.sort { |a, b| a <=> b } # ソート後、リスト要素は小さい順に並ぶ
|
||||
nums = nums.sort { |a, b| a <=> b } # ソート後、リストの要素は昇順に並ぶ
|
||||
```
|
||||
|
||||
??? pythontutor "可視化実行"
|
||||
??? 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
|
||||
https://pythontutor.com/render.html#code=%22%22%22%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%0A%20%20%20%20nums.sort()%20%20%23%20%E3%82%BD%E3%83%BC%E3%83%88%E5%BE%8C%E3%81%AF%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8C%E6%98%87%E9%A0%86%E3%81%AB%E4%B8%A6%E3%81%B6&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 つの設計ポイントを含める。
|
||||
リストの仕組みをより深く理解するため、次の三つを設計の要点とする簡単なリストを実装してみましょう。
|
||||
|
||||
- **初期容量**:妥当な配列の初期容量を選ぶ。この例では 10 を初期容量として選ぶ。
|
||||
- **要素数の記録**:`size` という変数を宣言して、現在のリスト要素数を記録し、要素の挿入と削除に応じてリアルタイムに更新する。この変数により、リスト末尾の位置を特定し、拡張が必要かどうかを判断できる。
|
||||
- **拡張機構**:要素を挿入する時点でリスト容量がいっぱいなら、拡張が必要になる。まず拡張倍率に応じてより大きな配列を作成し、次に現在の配列の全要素を順に新しい配列へ移す。この例では、配列を毎回以前の 2 倍に拡張する。
|
||||
- **初期容量**:配列の初期容量を適切に設定します。この例では 10 とします。
|
||||
- **要素数の記録**:現在の要素数を記録する変数 `size` を用意し、挿入と削除のたびに更新します。この変数からリストの末尾を特定し、容量拡張が必要かどうか判断できます。
|
||||
- **容量拡張の仕組み**:挿入時に容量がいっぱいなら、拡張する必要があります。まず拡張倍率に従って大きな配列を作り、現在の配列の全要素を順に移します。この例では、毎回、容量を元の 2 倍にします。
|
||||
|
||||
```src
|
||||
[file]{my_list}-[class]{my_list}-[func]{}
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,71 +1,71 @@
|
||||
# メモリとキャッシュ *
|
||||
|
||||
本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています。
|
||||
この章の最初の 2 節では、基本的で重要なデータ構造である配列と連結リストを取り上げました。これらはそれぞれ、「連続した格納」と「分散した格納」という二つの物理構造を表しています。
|
||||
|
||||
実際には、**物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し**、ひいてはアルゴリズムプログラム全体の性能に影響します。
|
||||
実際には、**物理構造によって、プログラムがメモリとキャッシュをどれだけ効率よく使えるかが大きく決まり**、ひいてはアルゴリズムを実装したプログラム全体の性能にも影響します。
|
||||
|
||||
## コンピュータの記憶装置
|
||||
|
||||
コンピュータには三種類の記憶装置があります。<u>ハードディスク(hard disk)</u>、<u>メモリ(random-access memory, RAM)</u>、<u>キャッシュ(cache memory)</u>です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。
|
||||
コンピュータには、<u>ハードディスク(hard disk)</u>、<u>メモリ(random-access memory, RAM)</u>、<u>キャッシュ(cache memory)</u>という 3 種類の記憶装置があります。下表に、コンピュータシステムにおけるそれぞれの役割と性能上の特徴を示します。
|
||||
|
||||
<p align="center"> 表 <id> コンピュータの記憶装置 </p>
|
||||
|
||||
| | ハードディスク | メモリ | キャッシュ |
|
||||
| | ハードディスク | メモリ | キャッシュ |
|
||||
| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
|
||||
| 用途 | OS、プログラム、ファイルなどを長期保存 | 実行中のプログラムや処理中のデータを一時保存 | 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす |
|
||||
| 揮発性 | 電源断後もデータは失われない | 電源断後にデータは失われる | 電源断後にデータは失われる |
|
||||
| 容量 | 大きい、TB 級 | 小さい、GB 級 | 非常に小さい、MB 級 |
|
||||
| 速度 | 遅い、数百〜数千 MB/s | 速い、数十 GB/s | 非常に速い、数十〜数百 GB/s |
|
||||
| 価格(人民元) | 比較的安価、数角〜数元 / GB | 比較的高価、数十〜数百元 / GB | 非常に高価、CPU と一体で価格設定される |
|
||||
| 用途 | OS、プログラム、ファイルなどのデータを長期間保存する | 実行中のプログラムと処理中のデータを一時的に保存する | 頻繁にアクセスするデータや命令を保存し、CPU がメモリにアクセスする回数を減らす |
|
||||
| 揮発性 | 電源を切ってもデータは失われない | 電源を切るとデータが失われる | 電源を切るとデータが失われる |
|
||||
| 容量 | 大きい、TB 単位 | 小さい、GB 単位 | 非常に小さい、MB 単位 |
|
||||
| 速度 | 遅い、数百〜数千 MB/s | 速い、数十 GB/s | 非常に速い、数十〜数百 GB/s |
|
||||
| 価格(人民元) | 安い、1 GB 当たり数角〜数元 | 高い、1 GB 当たり数十〜数百元 | 非常に高い、CPU に含めて価格が設定される |
|
||||
|
||||
コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。
|
||||
コンピュータの記憶システムは、下図のようなピラミッドとして捉えられます。頂点に近い記憶装置ほど高速で、容量が小さく、コストが高くなります。この階層的な設計は偶然生まれたものではなく、コンピュータ科学者や技術者が十分に考え抜いた結果です。
|
||||
|
||||
- **ハードディスクはメモリで置き換えにくい**。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
|
||||
- **キャッシュは大容量と高速性を両立しにくい**。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。
|
||||
- **ハードディスクをメモリで置き換えるのは困難です**。まず、メモリのデータは電源を切ると失われるため、長期保存には適していません。また、メモリのコストはハードディスクの数十倍であり、一般消費者向けに普及させるのが難しくなります。
|
||||
- **キャッシュの大容量化と高速化は両立が困難です**。L1、L2、L3 とキャッシュの容量が増えるにつれて物理的なサイズも大きくなり、CPU コアからの物理的な距離が遠くなります。その結果、データ転送にかかる時間が増え、要素へのアクセスの遅延も大きくなります。現在の技術では、キャッシュを階層化する構造が、容量、速度、コストの最もよいバランスを実現しています。
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。
|
||||
コンピュータの記憶階層は、速度、容量、コストの絶妙なバランスを表しています。実際、このようなトレードオフはあらゆる産業分野に存在し、それぞれの利点と制約の間で最適なバランスを見つけることが求められます。
|
||||
|
||||
要するに、**ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ**、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。
|
||||
全体として、**ハードディスクは大量のデータの長期保存に、メモリはプログラムの実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスするデータや命令の保存に使われます**。これらが連携してプログラムの実行効率を高め、コンピュータシステムの効率的な動作を支えます。
|
||||
|
||||
次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、**メモリからデータを賢く読み込むことで**、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。
|
||||
下図のように、プログラムの実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と考えることができます。**メモリから適切にデータを読み込むことで**、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、比較的遅いメモリへの依存を減らします。
|
||||
|
||||

|
||||
|
||||
## データ構造のメモリ効率
|
||||
|
||||
メモリ空間の利用という観点では、配列と連結リストにはそれぞれ利点と制約があります。
|
||||
メモリ空間の利用という点では、配列と連結リストにはそれぞれ利点と制約があります。
|
||||
|
||||
一方で、**メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません**。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列の要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストは「ノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。
|
||||
一つには、**メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできない**ため、データ構造にはできるだけ効率よく空間を使ってほしいものです。配列の要素は隙間なく並んでおり、連結リストのノード間の参照(ポインタ)を保存する追加の空間が必要ないため、空間効率が高くなります。ただし、配列は十分な連続メモリ領域を一度に確保する必要があり、メモリを無駄にする可能性があります。また、配列の容量拡張には追加の時間と空間のコストがかかります。一方、連結リストは「ノード」単位でメモリを動的に確保・解放できるため、柔軟性に優れています。
|
||||
|
||||
他方で、プログラムの実行中には、**メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み**、メモリ利用効率の低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リストの要素は分散して格納されるため、頻繁な挿入や削除を行うと、より断片化を招きやすくなります。
|
||||
もう一つには、プログラムの実行中、**メモリの確保と解放を繰り返すにつれて、空きメモリの断片化が進み**、メモリの利用効率が低下します。配列は連続して格納されるため、比較的断片化を起こしにくくなります。対して連結リストの要素は分散して格納されるため、挿入と削除を頻繁に行うと、メモリの断片化が起きやすくなります。
|
||||
|
||||
## データ構造のキャッシュ効率
|
||||
|
||||
キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュ内に存在しない場合、<u>キャッシュミス(cache miss)</u>が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません。
|
||||
キャッシュはメモリより容量がはるかに小さいものの、非常に高速であり、プログラムの実行速度に決定的な役割を果たします。容量が限られているため、頻繁にアクセスするデータの一部しか保存できません。CPU がアクセスしようとしたデータがキャッシュにないと、<u>キャッシュミス(cache miss)</u>が発生し、CPU は必要なデータを比較的遅いメモリから読み込まなければなりません。
|
||||
|
||||
当然ながら、**「キャッシュミス」が少ないほど、CPU のデータ読み書き効率は高くなり**、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合を<u>キャッシュヒット率(cache hit rate)</u>と呼び、この指標は通常、キャッシュ効率の評価に用いられます。
|
||||
当然、**「キャッシュミス」が少ないほど CPU のデータ読み書きの効率が高くなり**、プログラムの性能も向上します。CPU がキャッシュからデータを取得できた割合を<u>キャッシュヒット率(cache hit rate)</u>といい、通常はキャッシュの効率を測る指標として使います。
|
||||
|
||||
できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。
|
||||
効率をできるだけ高めるため、キャッシュは次のような仕組みでデータを読み込みます。
|
||||
|
||||
- **キャッシュライン**:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
|
||||
- **プリフェッチ機構**:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
|
||||
- **空間的局所性**:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
|
||||
- **時間的局所性**:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。
|
||||
- **キャッシュライン**:キャッシュはデータを 1 バイトずつ保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイトずつ転送するよりも、キャッシュライン単位で転送する方が効率的です。
|
||||
- **プリフェッチ**:プロセッサはデータへのアクセスパターン(順次アクセス、一定間隔で飛び飛びに行うアクセスなど)を予測し、そのパターンに基づいてデータをキャッシュに読み込み、ヒット率を高めます。
|
||||
- **空間的局所性**:あるデータにアクセスすると、その近くのデータにも近いうちにアクセスする可能性があります。そのため、キャッシュはあるデータを読み込むときに周辺のデータも読み込み、ヒット率を高めます。
|
||||
- **時間的局所性**:あるデータにアクセスすると、近い将来に再びアクセスする可能性が高くなります。キャッシュはこの性質を利用し、最近アクセスしたデータを保持してヒット率を高めます。
|
||||
|
||||
実際には、**配列と連結リストではキャッシュの利用効率が異なり**、主に次の点に表れます。
|
||||
実際、**配列と連結リストではキャッシュの利用効率が異なります**。主な違いは次のとおりです。
|
||||
|
||||
- **使用空間**:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。
|
||||
- **キャッシュライン**:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
|
||||
- **プリフェッチ機構**:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
|
||||
- **空間的局所性**:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。
|
||||
- **使用する空間**:連結リストの要素は配列の要素より多くの空間を使うため、キャッシュに収まる有効なデータの量が少なくなります。
|
||||
- **キャッシュライン**:連結リストのデータはメモリの各所に分散していますが、キャッシュは「ライン単位で読み込む」ため、不要なデータを読み込む割合が高くなります。
|
||||
- **プリフェッチ**:配列のデータアクセスパターンは連結リストより「予測しやすく」、次に読み込むデータをシステムが推測しやすくなります。
|
||||
- **空間的局所性**:配列はまとまったメモリ領域に格納されるため、読み込んだデータの近くにあるデータにも、まもなくアクセスする可能性が高くなります。
|
||||
|
||||
全体として、**配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています**。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。
|
||||
全体として、**配列はキャッシュヒット率が高いため、通常は連結リストより操作効率に優れています**。そのため、アルゴリズムの問題を解く際には、配列を使って実装したデータ構造が好まれる傾向にあります。
|
||||
|
||||
注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。
|
||||
ただし、**キャッシュ効率が高いからといって、あらゆる場合に配列が連結リストより優れているわけではありません**。実際の用途では、具体的な要件に応じてデータ構造を選ぶ必要があります。たとえば、「スタック」というデータ構造(次の章で詳しく説明します)は配列でも連結リストでも実装できますが、それぞれに適した場面は異なります。
|
||||
|
||||
- アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
|
||||
- データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
|
||||
- アルゴリズムの問題を解くときは、操作効率が高くランダムアクセスも可能な、配列によるスタックを選ぶことが多いです。その代わりに必要なのは、あらかじめ配列用のメモリ領域をある程度確保しておくことだけです。
|
||||
- データ量が非常に大きく、変動が激しく、スタックの大きさを予測しにくい場合には、連結リストによるスタックが適しています。連結リストなら大量のデータをメモリの各所に分散して格納でき、配列の容量拡張に伴う追加のコストも避けられます。
|
||||
|
||||
@@ -2,85 +2,85 @@
|
||||
|
||||
### 要点の振り返り
|
||||
|
||||
- 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。
|
||||
- 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。
|
||||
- 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。
|
||||
- リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。
|
||||
- リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。
|
||||
- プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。
|
||||
- キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる。
|
||||
- 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである。
|
||||
- 配列と連結リストは二つの基本的なデータ構造であり、コンピュータのメモリにデータを格納する二つの方法、すなわち連続した領域への格納と分散した領域への格納をそれぞれ表します。両者の特徴は互いに補い合う関係にあります。
|
||||
- 配列はランダムアクセスをサポートし、メモリ使用量も少なく済みます。ただし、要素の挿入と削除の効率は低く、初期化後に長さを変えることはできません。
|
||||
- 連結リストは参照(ポインタ)の変更によりノードを効率よく挿入・削除でき、長さも柔軟に調整できます。ただし、ノードへのアクセス効率は低く、メモリ使用量が多くなります。代表的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあります。
|
||||
- リストは、挿入・削除・検索・更新に対応する、順序を持つ要素の集合です。通常は動的配列で実装されます。配列の利点を保ちながら、長さを柔軟に調整できます。
|
||||
- リストの登場により配列の実用性は大幅に向上しましたが、メモリ領域の一部が無駄になる可能性もあります。
|
||||
- プログラムの実行時、データは主にメモリに格納されます。配列はメモリ空間を効率よく使えますが、連結リストはメモリをより柔軟に使えます。
|
||||
- キャッシュは、キャッシュライン、プリフェッチ、空間的局所性、時間的局所性などの仕組みでデータを読み込み、CPU に高速なデータアクセスを提供して、プログラムの実行効率を大幅に高めます。
|
||||
- 配列はキャッシュヒット率が高いため、通常は連結リストより効率的です。データ構造は、具体的な要件や場面に応じて適切に選ぶ必要があります。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか?
|
||||
**Q**:配列をスタックに置く場合とヒープに置く場合で、時間効率や空間効率に違いはありますか。
|
||||
|
||||
スタック上とヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作の効率は基本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じる。
|
||||
スタック上でもヒープ上でも、配列は連続したメモリ領域に格納されるため、データ操作の効率は基本的に同じです。ただし、スタックとヒープにはそれぞれ特徴があり、次のような違いが生じます。
|
||||
|
||||
1. 確保と解放の効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コード内で動的に確保できる反面、断片化しやすい。そのため、ヒープ上での確保と解放は通常スタック上より遅い。
|
||||
2. サイズ制限:スタックメモリは比較的小さく、ヒープのサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適している。
|
||||
3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があるが、ヒープ上の配列サイズは実行時に動的に決定できる。
|
||||
1. 確保と解放の効率:スタックは比較的小さなメモリ領域で、割り当てはコンパイラが自動的に行います。一方、ヒープは比較的大きく、コードから動的に確保できる反面、断片化しやすくなります。そのため、ヒープでの確保と解放は通常、スタックより遅くなります。
|
||||
2. サイズの制限:スタックのメモリは比較的小さく、ヒープの大きさは通常、利用可能なメモリに制限されます。そのため、大きな配列を格納するにはヒープの方が適しています。
|
||||
3. 柔軟性:スタック上の配列はコンパイル時に大きさを決める必要がありますが、ヒープ上の配列は実行時に動的に決められます。
|
||||
|
||||
**Q**:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか?
|
||||
**Q**:配列では要素が同じ型でなければならないのに、連結リストでは同じ型であることを強調しないのはなぜですか。
|
||||
|
||||
連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには `int`、`double`、`string`、`object` など、異なる型のデータを格納できる。
|
||||
連結リストはノードで構成され、ノード同士が参照(ポインタ)でつながっています。各ノードには `int`、`double`、`string`、`object` など、異なる型のデータを格納できます。
|
||||
|
||||
これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に `int` と `long` の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。
|
||||
一方、配列ではオフセットを計算して要素の位置を求めるため、要素が同じ型でなければなりません。たとえば配列に `int` と `long` の両方が含まれ、1 要素の大きさがそれぞれ 4 バイトと 8 バイトだとします。この場合は「要素の長さ」が 2 種類あるため、次の式でオフセットを計算できなくなります。
|
||||
|
||||
```shell
|
||||
# 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス
|
||||
# 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素の長さ * 要素のインデックス
|
||||
```
|
||||
|
||||
**Q**:ノード `P` を削除した後、`P.next` を `None` に設定する必要はありますか?
|
||||
**Q**:ノード `P` を削除した後、`P.next` を `None` にする必要はありますか。
|
||||
|
||||
`P.next` を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや `P` に出会うことはない。つまり、ノード `P` はすでに連結リストから削除されており、この時点で `P` がどこを指していても、この連結リストには影響しない。
|
||||
`P.next` は変更しなくてもかまいません。その連結リストから見ると、先頭ノードから末尾ノードまで走査しても、もはや `P` には出会いません。つまり、ノード `P` はすでに連結リストから削除されており、どこを指していてもその連結リストには影響しません。
|
||||
|
||||
データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。
|
||||
データ構造とアルゴリズムの問題を解くという観点では、プログラムのロジックが正しければ、参照を切らなくても問題ありません。標準ライブラリという観点では、切り離す方が安全で、ロジックも明確になります。参照を切らず、削除したノードが適切に回収されなかった場合、その後続ノードのメモリ回収に影響します。
|
||||
|
||||
**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**:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?
|
||||
**Q**:「連結リストの定義と格納方法」の図にある、水色のノードへのポインタは、一つのメモリアドレスに相当する領域を占めるのでしょうか。それともノードの値と半分ずつ占めるのでしょうか。
|
||||
|
||||
この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。
|
||||
この図は定性的な模式図です。実際の大きさは、具体的な状況に応じて検討する必要があります。
|
||||
|
||||
- ノード値が占める領域は型によって異なり、たとえば `int`、`long`、`double`、インスタンスオブジェクトなどがある。
|
||||
- ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。
|
||||
- ノードの値が占める空間は、`int`、`long`、`double`、インスタンスオブジェクトなど、型によって異なります。
|
||||
- ポインタ変数が占めるメモリの大きさは、使用する OS とコンパイル環境によって決まり、多くの場合は 8 バイトまたは 4 バイトです。
|
||||
|
||||
**Q**:リストの末尾への要素追加は常に $O(1)$ ですか?
|
||||
**Q**:リストの末尾に要素を追加する操作は、常に $O(1)$ ですか。
|
||||
|
||||
要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は $O(n)$ になる。
|
||||
追加によってリストの長さを超える場合は、まずリストの容量を拡張してから追加する必要があります。システムは新しいメモリ領域を確保し、元のリストの全要素を移すため、このときの時間計算量は $O(n)$ になります。
|
||||
|
||||
**Q**:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?
|
||||
**Q**:「リストの登場により配列の実用性は大幅に向上しましたが、メモリ領域の一部が無駄になる可能性もあります」とあります。この無駄とは、容量、長さ、拡張倍率などの追加の変数が占めるメモリのことですか。
|
||||
|
||||
ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば $\times 1.5$ を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。
|
||||
ここでいう空間の無駄には、主に二つの意味があります。一つは、リストに設定する初期の長さが、実際に必要な量より大きい場合があることです。もう一つは、頻繁な容量拡張を避けるため、通常は $\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 では数値もオブジェクトとして扱われます。リストには数値自体ではなく、数値への参照が格納されます。そのため、二つの配列にある同じ数値の id が一致し、それらの数値のメモリアドレスが連続している必要もないのです。
|
||||
|
||||
**Q**:C++ STL の `std::list` はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか?
|
||||
**Q**:C++ STL の `std::list` は双方向連結リストを実装していますが、アルゴリズムの本ではあまり直接使われていないようです。何か制約があるのでしょうか。
|
||||
|
||||
一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。
|
||||
一つには、アルゴリズムの実装では配列が好まれ、連結リストは必要な場合にだけ使うことが多いためです。主な理由は二つあります。
|
||||
|
||||
- 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list` は通常 `std::vector` より多くの空間を消費する。
|
||||
- キャッシュ非効率:データが連続して格納されていないため、`std::list` はキャッシュの利用効率が低い。一般には、`std::vector` のほうが性能がよい。
|
||||
- 空間コスト:各要素に追加で二つのポインタ(前の要素用と次の要素用)が必要なため、`std::list` は通常、`std::vector` より多くの空間を使います。
|
||||
- キャッシュとの相性:データが連続して格納されていないため、`std::list` はキャッシュの利用効率が低くなります。一般に、`std::vector` の方が高い性能を得られます。
|
||||
|
||||
もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する `stack` と `queue` を使う。
|
||||
もう一つには、連結リストを使う必要があるのは主に二分木やグラフの場合だという点があります。スタックやキューには、連結リストではなく、プログラミング言語が提供する `stack` や `queue` を使うことが多いです。
|
||||
|
||||
**Q**:`res = [[0]] * n` という操作で 2 次元リストを生成した場合、それぞれの `[0]` は独立していますか?
|
||||
**Q**:`res = [[0]] * n` で作った二次元リストでは、それぞれの `[0]` は独立していますか。
|
||||
|
||||
独立していない。この 2 次元リストでは、すべての `[0]` は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。
|
||||
独立していません。この二次元リストでは、すべての `[0]` が実際には同じオブジェクトへの参照です。一つの要素を変更すると、対応するすべての要素も変わります。
|
||||
|
||||
2 次元リスト内の各 `[0]` を独立させたい場合は、`res = [[0] for _ in range(n)]` を使って実現できる。この方式の原理は、独立した `[0]` リストオブジェクトを $n$ 個初期化していることにある。
|
||||
二次元リストの各 `[0]` を独立させたい場合は、`res = [[0] for _ in range(n)]` とします。この方法では、独立した `[0]` というリストオブジェクトを $n$ 個初期化します。
|
||||
|
||||
**Q**:`res = [0] * n` という操作で生成されたリストでは、それぞれの整数 0 は独立していますか?
|
||||
**Q**:`res = [0] * n` で作ったリストでは、それぞれの整数 0 は独立していますか。
|
||||
|
||||
このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。
|
||||
このリストでは、すべての整数 0 が同じオブジェクトへの参照です。Python は小さな整数(通常は -5 から 256)をキャッシュプールで管理し、オブジェクトをできるだけ再利用して性能を高めているためです。
|
||||
|
||||
それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。
|
||||
同じオブジェクトを指していても、リストの各要素は独立に変更できます。Python の整数が「不変オブジェクト」だからです。要素を変更するときは、元のオブジェクト自体を変えるのではなく、別のオブジェクトへの参照に切り替えています。
|
||||
|
||||
しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。
|
||||
ただし、リストの要素が「可変オブジェクト」(リスト、辞書、クラスのインスタンスなど)の場合、その要素を変更するとオブジェクト自体が変わり、それを参照するすべての要素に同じ変更が反映されます。
|
||||
|
||||