著者: 禅とコンピュータープログラミングの芸術
1. 背景の紹介
データ構造とその応用
データ構造はコンピュータ プログラミングにおいて重要な役割を果たし、相互に 1 つ以上の関係を持つデータ要素の集合、およびこれらの要素を操作するための規則や方法を指します。線形テーブル、ツリー構造、グラフ構造、キュー、スタック、ハッシュ テーブル、セット、ヒープ、ソート アルゴリズムなど。データ構造が異なれば、パフォーマンス、記憶容量、実装難易度などの測定基準も異なります。したがって、プログラムの効率を向上させ、プログラムのリソース消費を削減するには、適切なデータ構造を選択することが非常に重要です。
実際、データ構造はプログラムのパフォーマンスと効率に直接影響するだけでなく、プログラム設計にも不可欠な部分です。たとえば、間違ったデータ構造を選択すると、プログラムが正しく実行されなかったり、動作が遅くなったり、期待した結果が得られなかったりする可能性があるため、データ構造を適用する前に、データのサイズ、アプリケーションのシナリオ、アルゴリズムの複雑さを十分に考慮する必要があります。 、同時読み取りと書き込みなど、多くの要因があります。さらに、さまざまなデータ構造に習熟すると、さまざまなアルゴリズムの動作原理をより深く理解し、それらをより適切に使用して実際的な問題を解決するのに役立ちます。
どのデータ構造を選択するか
一般的に、すでにある程度の経験がある場合は、既存のコードまたは抽象的なビジネス ロジックを分析することで、どのデータ構造を使用するかを決定できます。しかし多くの場合、特定の問題が発生した場合にのみ、特定のデータ構造の長所と短所、適用可能なシナリオ、実装を詳しく調べます。たとえば、大量のデータを処理する必要がある場合、キーワードをすばやく見つけるためにハッシュ テーブルを使用するのが良い選択であり、データ項目を頻繁に挿入、削除、または変更する必要がある場合は、ツリー構造を使用することができます。 (赤黒ツリー、B ツリーなど)、メモリを動的に管理する必要がある場合、スタックまたは両端キューを使用でき、データのサイズが比較的小さい場合 (数百のデータ項目など) ) ランダム アクセスが必要な場合は、配列を使用することをお勧めします。全体として、適切なデータ構造を選択すると、プログラムのパフォーマンスが効果的に向上し、メモリ領域が節約され、リソース消費が削減されます。
さらに、新しい要件が発生した場合は、実際の状況に応じて関連するアルゴリズムの原則と時間計算量を比較検討し、シナリオに基づいて最適なデータ構造を選択することもできます。これにより、ニーズを可能な限り満たすだけでなく、プログラムの正確性と効率性も確保できます。
2. 中心となる概念とつながり
データの種類
データ型は通常、サイズ、表現、範囲、単位、精度、NULL 許容値、エンコード方法などを含むデータの特性を指します。一般的なデータ型には、整数、浮動小数点、文字、ブール値、日付、配列、ポインタ、構造体、共用体などが含まれます。
シーケンステーブル
シーケンス テーブルは、同じ種類の変数をまとめて格納し、線形構造に配置する記憶構造です。順序テーブルの各データは順番に格納されます。シーケンシャル テーブルは通常、要素の挿入と削除という 2 つの基本操作を実行できます。
静的シーケンステーブル
静的シーケンス テーブルは配列です。つまり、各位置に 1 つの要素だけが格納されます。静的シーケンシャル テーブルは挿入と削除の操作を簡単に実行できますが、記憶域スペースの割り当てと解放のオーバーヘッドが高いため、データ項目が多い場合はクエリ速度が制限されます。
動的シーケンステーブル
動的シーケンス テーブルは静的シーケンス テーブルに似ていますが、動的な拡張と縮小が可能です。つまり、追加された要素が現在の記憶容量を超えると、テーブルは自動的により多くの記憶領域を再割り当てします。動的シーケンシャル テーブルは、場合によっては静的シーケンシャル テーブルよりも高いクエリ速度を実現できます。
リンクされたリスト
リンク リストは非連続ストレージ構造であり、データ項目の値を格納するだけでなく、リンク リストの各ノードには次のノードを指すポインタ フィールドもあります。リンクリストの挿入や削除の操作は面倒ですが、柔軟な操作が可能です。リンク リストの挿入と削除は O(1) 時間以内に完了できます。これは、高速な挿入と削除を必要とする特定のアプリケーションにとって非常に重要です。
ハッシュ表
ハッシュ テーブルは、キーと値のペアをマッピングしたデータ構造であり、インデックス値を計算して記憶領域内の要素の位置を取得することで、検索時間を大幅に短縮します。ハッシュ関数は、任意の長さのバイナリ値をインデックス値に変換できます。一般的に使用されるハッシュ関数には、除算剰余法、積法、二乗検出法、チェーンアドレス法、オープンアドレス法などがあります。
スタック
スタックは、先頭でのみ挿入および削除操作を実行できる特殊な線形リストです。スタックの最上部にある要素はスタックの最上部と呼ばれ、スタックの最下部にある要素はスタックの最下部と呼ばれます。スタックは後入れ先出し (LIFO) データ構造です。
列
キューは、キューの最後にのみ挿入でき、キューの先頭で削除できる特別な線形テーブルです。チームの先頭にある要素はチームの先頭と呼ばれ、チームの最後にある要素は末尾と呼ばれます。キューは先入れ先出し (FIFO) データ構造です。
集める
セットは、重複した要素を含まない順序付けされていないコレクションです。これは主に、要素がセットに属しているかどうかを判断したり、2 つのセット間の差分や和集合を見つけたりするなど、要素のセットに対していくつかの基本的な操作を実行するために使用されます。
ヒープ
ヒープは特別なツリー データ構造です。ルート ノードは最大 (最小) の値を持ちます。他のすべてのノードはバイナリ ツリーの定義を満たす必要があり、左側のサブツリーの値は、バイナリ ツリーの値以下です。右のサブツリー。ヒープには、スケジューリング アルゴリズム、優先キューなど、さまざまな用途があります。
優先キュー
プライオリティキューは、データを優先的に処理するために使用される特別なキューです。優先キューは、要素を優先度に従ってソートし、先頭の要素が削除されるたびに、最も高い優先度を持つ要素になります。共通の優先キューには、min-heap と max-heap の 2 つがあります。
3. コアアルゴリズムの原理、具体的な操作手順、数学モデルの公式の詳細な説明
ソートアルゴリズム
ソート アルゴリズムは、レコード コレクションを再配置するために使用されるアルゴリズムを指します。一般的なソート アルゴリズムには、バブル ソート、挿入ソート、選択ソート、ヒル ソート、マージ ソート、クイック ソート、ヒープ ソート、カウンティング ソート、バケット ソート、基数ソートなどがあります。
挿入ソート
挿入ソートは単純なソート アルゴリズムです。その中心的な考え方は、ソートされる一連の数値において、前の n-1 個の数値がソートされていると仮定して、今度は n 番目の数値をそれに挿入する必要があるということです。つまり、これらの n は数値は引き続きソートされます。アルゴリズムは次のとおりです。
- 最初の要素から始めて、要素はソートされていると考えることができます。
- 次の要素を取り出し、ソートされた一連の要素を後ろから前にスキャンします。
- (ソートされた) 要素が新しい要素より大きい場合、要素を次の位置に移動します
- 並べ替えられた要素が新しい要素以下になる位置が見つかるまで、手順 3 を繰り返します。
- その位置に新しい要素を挿入した後
- 手順2~5を繰り返します
サンプルコード:
public void insertionSort(int[] arr){
int n = arr.length;
for(int i=1;i<n;i++){
int key = arr[i]; // 待插入元素
int j = i - 1; // 有序序列最后一个元素下标
while(j>=0 && arr[j]>key){
arr[j+1] = arr[j]; // 移动元素至后一个位置
j--; // 更新下标
}
arr[j+1] = key; // 插入新元素
}
}
平均時間計算量: O(n^2)
バブルソート
バブル ソートは、シンプルで安定したソート アルゴリズムです。その中心となるアイデアは、ソート対象のシーケンスを繰り返し訪問し、一度に 2 つの要素を比較し、比較する必要のある数値のペアがなくなるまでそれらの位置を交換することです。 。アルゴリズムは次のとおりです。
- 隣接する要素を比較します。最初の要素が 2 番目の要素より大きい場合は、両方の要素を交換します。
- 隣接する要素の各ペアに対して、最初の最初のペアから最後の最後のペアまで同じことを行い、最後の要素が最大の番号になるようにします。
- 最後の要素を除くすべての要素に対して上記の手順を繰り返します。
- 比較する数値のペアがなくなるまで、要素の数を減らしながら上記の手順を繰り返します。
サンプルコード:
public void bubbleSort(int[] arr){
int n = arr.length;
for(int i=0;i<n-1;i++){
for(int j=0;j<n-i-1;j++){
if(arr[j]>arr[j+1]){
// swap arr[j] and arr[j+1]
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
平均時間計算量: O(n^2)
選択ソート
選択ソートは、シンプルで直感的な並べ替えアルゴリズムです。その中心となるアイデアは、最小 (最大) の要素を見つけて最初に配置し、次に残りの要素から最小 (最大) の要素を見つけて 2 番目の位置に配置することです。ソートが完了するまで続きます。アルゴリズムは次のとおりです。
- ソートされていないシーケンス内で最小 (最大) の要素を見つけ、それをソートされたシーケンスの先頭に格納します。
- 次に、ソートされていない残りの要素から最小 (最大) の要素を検索し、それをソートされたシーケンスの最後に置きます。
- すべての要素が並べ替えられるまで手順 2 を繰り返します
サンプルコード:
public void selectionSort(int[] arr){
int n = arr.length;
for(int i=0;i<n-1;i++){
int minIndex = i;
for(int j=i+1;j<n;j++){
if(arr[minIndex]>arr[j]){
minIndex = j;
}
}
// swap arr[i] and arr[minIndex]
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
平均時間計算量: O(n^2)
ヒルソート
シェル ソートは、挿入ソートのより効率的かつ改良されたバージョンであり、その中心的なアイデアは、添え字の特定の増分によってレコードをグループ化し、直接挿入ソート アルゴリズムを使用して各グループをソートすることです。増分が徐々に減少するにつれて、並べ替えプロセス全体がより秩序正しく効率的になります。アルゴリズムは次のとおりです。
- ギャップ シーケンスを設定し、ギャップ シーケンスの最後の要素を配列の長さの半分に設定します。
- 各ギャップの要素をループを通して対応する位置に挿入します
- ギャップが減少し、新しいギャップが設定されます。
- ギャップが 1 の場合、シーケンス全体がソートされ、ループが終了します。
サンプルコード:
public void shellSort(int[] arr){
int n = arr.length;
// 计算间隔序列
int d = 1;
while((d/9)*7 < n/3){
d = (d*3 + 1)/2;
}
// 进行希尔排序
for(int g=d;g>0;g/=3){
for(int i=g;i<n;i+=g){
int temp = arr[i];
int j;
for(j=i;j>=g && arr[j-g]>temp;j-=g){
arr[j] = arr[j-g];
}
arr[j] = temp;
}
}
}
平均時間計算量: O(n^(3/2))
マージソート
マージ ソートは、マージ操作に基づいた効果的なソート アルゴリズムであり、安定したパフォーマンスを備えた再帰的ソート アルゴリズムです。マージソートは、まず現在のシーケンスを再帰的に 2 つの半分に分割し、次に両側を別々にソートし、次に 2 つのソート結果をマージします。アルゴリズムは次のとおりです。
- 長さ n の入力シーケンスを長さ n/2 の 2 つのサブシーケンスに分割します。
- これら 2 つのサブシーケンスに対してマージ ソートを呼び出します。
- 2 つのソートされたサブシーケンスを最終的なソートされたシーケンスにマージします。
サンプルコード:
public void mergeSort(int[] arr){
int n = arr.length;
if(n<=1){
return;
}
int mid = n / 2;
int[] leftArr = new int[mid];
int[] rightArr = new int[n - mid];
System.arraycopy(arr, 0, leftArr, 0, mid);
System.arraycopy(arr, mid, rightArr, 0, n - mid);
mergeSort(leftArr);
mergeSort(rightArr);
merge(arr, leftArr, rightArr);
}
private static void merge(int[] arr, int[] leftArr, int[] rightArr){
int i = 0;
int j = 0;
int k = 0;
while(i<leftArr.length&&j<rightArr.length){
if(leftArr[i]<rightArr[j]){
arr[k++] = leftArr[i++];
}else{
arr[k++] = rightArr[j++];
}
}
while(i<leftArr.length){
arr[k++] = leftArr[i++];
}
while(j<rightArr.length){
arr[k++] = rightArr[j++];
}
}
平均時間計算量: O(nlogn)
クイックソート
クイック ソートはバブル ソートを改良したもので、分割統治戦略を使用してシリアル配列 (またはリスト) を 2 つのサブシーケンスに分割し、一部の要素のソート コードが要素の別の部分のソート コードより大きくなります。小さい。次に、シーケンス全体が整うまでこの方法で並べ替えます。アルゴリズムは次のとおりです。
- シーケンスから「ピボット」と呼ばれる要素を選択します。
- 基本値より小さいすべての要素が左側に配置され、基本値より大きいすべての要素が右側に配置されるようにシーケンスを並べ替えます (同じ数値をどちらの側にも配置できます)。この分割の後、参照要素の左側または右側はソートされた部分配列になります。
- すべてのサブ配列がソートされるまで、両側のサブ配列に対してステップ 2 を繰り返します。
- アレイ全体が整うまで、アレイ全体に対して最初のステップを繰り返します。
サンプルコード:
public void quickSort(int[] arr, int start, int end){
if(start >= end){
return;
}
int pivotIdx = partition(arr, start, end);
quickSort(arr, start, pivotIdx-1);
quickSort(arr, pivotIdx+1, end);
}
// 返回arr[l...r]的第k大元素的索引
private static int partition(int[] arr, int l, int r){
int v = arr[l];
int idx = l;
for(int i=l+1;i<=r;i++){
if(arr[i] > v){
idx++;
swap(arr, idx, i);
}
}
swap(arr, idx, l);
return idx;
}
private static void swap(int[] arr, int a, int b){
int t = arr[a];
arr[a] = arr[b];
arr[b] = t;
}
平均時間計算量: O(nlogn)、最悪の場合 O(n^2)
ヒープソート
ヒープ ソートは、ヒープなどのデータ構造を使用して実装されたソート アルゴリズムを指します。ヒープは、ほぼ完全なバイナリ ツリーである構造です。ツリー内の各ノードは、親ノードの値が以下であることを満たします。子ノードの値、およびヒープの場合、この機能により、配列を使用してヒープを実装し、動的に維持できるようになります。アルゴリズムは次のとおりです。
- ヒープの作成 (最大ヒープまたは最小ヒープ)
- ヒープの先頭要素(最大値または最小値)を配列の最後に配置し、ヒープを調整します
- ヒープが空になるまで手順 2 を繰り返します
- ヒープソートが完了しました
サンプルコード:
public void heapSort(int[] arr){
int len = arr.length;
buildMaxHeap(arr, len); // 创建最大堆
for(int i=len-1;i>0;i--){
swap(arr, 0, i); // 堆顶元素放到末尾
siftDown(arr, 0, i); // 堆调整
}
}
private static void buildMaxHeap(int[] arr, int len){
for(int i=(len-2)/2;i>=0;i--){
siftDown(arr, i, len);
}
}
private static void siftDown(int[] arr, int i, int len){
int child;
int tmp = arr[i];
for(child=2*i+1;child<len;child=2*child+1){
if(child!=len-1 && arr[child]<arr[child+1]){ // 若有右孩子且右孩子大于左孩子
child++;
}
if(tmp<arr[child]){
break;
}
arr[i] = arr[child];
i = child;
}
arr[i] = tmp;
}
平均時間計算量: O(nlogn)
カウントソート
カウンティング ソートは、非比較ソート アルゴリズムです。その中心的な考え方は、配列内の各要素の出現数を数え、実際の状況に応じて対応するサイズの配列を作成し、各要素が O( 1) 時間内に出力配列に値を割り当てます。アルゴリズムは次のとおりです。
- ソートする配列要素の範囲を決定します。配列要素の最小値を m、最大値を M として、サイズが M-m+1 の配列 C を作成します。
- 配列 C のすべての要素を 0 に初期化します。
- 配列 A を走査し、各要素の値をインデックスとして使用し、対応する C[x] 値に 1 を加算します。
- C[x]の値に基づいて配列Bを再構築します
- 配列 B を返す
サンプルコード:
public void countingSort(int[] arr){
int maxVal = Integer.MIN_VALUE;
int minVal = Integer.MAX_VALUE;
for(int val : arr){
maxVal = Math.max(maxVal, val);
minVal = Math.min(minVal, val);
}
int countArraySize = maxVal - minVal + 1;
int[] countArray = new int[countArraySize];
for(int val : arr){
countArray[val - minVal]++;
}
int pos = 0;
for(int i=0;i<countArraySize;i++){
while(countArray[i]>0){
arr[pos++] = i + minVal;
countArray[i]--;
}
}
}
平均時間計算量: O(n+k)
バケットソート
バケット ソートは、カウンティング ソートの拡張アルゴリズムであり、関数のマッピング関係を使用して、ソート対象のデータをいくつかの異なるバケットに分割し、各バケット内のデータを個別にソートします。アルゴリズムは次のとおりです。
- n バケットがあると仮定して固定数のバケットを定義し、元の配列 A を走査し、要素 e をバケット A[(e-min)/(max-min)*(n-1)] に分割します。
- バケット i を走査し、バケット i 内の要素を並べ替えます。挿入並べ替え、選択並べ替え、バブル 並べ替えなどの任意の並べ替えアルゴリズムを使用できます。
- すべてのバケットをマージして、完全にソートされた配列を取得します。
サンプルコード:
public void bucketSort(int[] arr){
int numBuckets = 10;
// Determine minimum and maximum values in the array
int minValue = Integer.MAX_VALUE;
int maxValue = Integer.MIN_VALUE;
for(int value : arr){
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
// Create list of empty buckets to hold elements
List<List<Integer>> bucketList = new ArrayList<>();
for(int i=0; i<numBuckets; i++) {
bucketList.add(new ArrayList<>());
}
// Distribute each element into its appropriate bucket based on its value range
double bucketSize = ((double)(maxValue - minValue + 1))/numBuckets;
for(int value : arr) {
int index = (int)Math.floor(((value - minValue)/bucketSize));
if(index == numBuckets) {
index--;
}
bucketList.get(index).add(value);
}
// Sort each non-empty bucket using an Insertion Sort
for(List<Integer> bucket : bucketList) {
Collections.sort(bucket);
}
// Merge all sorted buckets together into one sorted array
int index = 0;
for(List<Integer> bucket : bucketList) {
for(int value : bucket) {
arr[index++] = value;
}
}
}
平均時間計算量: O(n+k)、ここで k はバケットの数です。
基数ソート
Radix Sort は非比較ソート アルゴリズムであり、その中心となるアイデアは、整数を桁ごとに切り取り、各桁の数値の順序で並べ替えることです。アルゴリズムは次のとおりです。
- 配列内の最大の要素を取得し、最大桁数を決定します。
- dをループインデックスとして、最下位ビットから順に、ソート対象の系列に対して安定したソートを実行します。
- 各サイクルの終わりに、シーケンスの最大ビット値が変化します。つまり、最大ビット値の範囲が小さくなります。
- 合計 d ラウンドのループが実行されます
サンプルコード:
public void radixSort(int[] arr) {
int m = getMax(arr);
boolean negative = false;
if(m < 0) {
negative = true;
m *= -1;
}
for(int exp = 1; m/exp > 0; exp*=10) {
countingSort(arr, exp, digitAtPosition(arr, arr[0], exp), negative);
}
if(negative) {
reverse(arr, 0, arr.length-1);
}
}
private static int getMax(int[] arr) {
int max = arr[0];
for(int i = 1; i < arr.length; i++) {
if(arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static void countingSort(int[] arr, int exp, int divisor, boolean negative) {
int size = 10;
int[] count = new int[size];
// Count frequencies of digits at position 'exp'
for(int i = 0; i < arr.length; i++) {
int digit = getDigit(arr[i], exp, negative);
if(digit == 0) {
continue;
}
count[digit]++;
}
// Calculate prefix sums of counts
for(int i = 1; i < size; i++) {
count[i] += count[i-1];
}
// Place each element in its correct place in output array
int[] output = new int[arr.length];
for(int i = arr.length-1; i >= 0; i--) {
int digit = getDigit(arr[i], exp, negative);
if(digit!= 0) {
output[count[digit]-1] = arr[i];
count[digit]--;
}
}
// Copy sorted elements back into original array
System.arraycopy(output, 0, arr, 0, arr.length);
}
private static int getDigit(int number, int exp, boolean negative) {
int digit = (number/exp)%10;
if(!negative) {
return digit;
} else {
if(digit == 0) {
return 10;
} else {
return digit * (-1);
}
}
}
private static void reverse(int[] arr, int low, int high) {
while(low < high) {
int temp = arr[low];
arr[low] = arr[high];
arr[high] = temp;
low++;
high--;
}
}
平均時間計算量: O(nk)、ここで k は配列内の要素の最大桁数です。