ソートアルゴリズムの時間計算量と空間計算量

基数ソートアルゴリズム:

一、ソートアルゴリズムの時間計算量と空間計算量

**ソートアルゴリズム** **平均時間計算量** **最悪時間計算量** **最良時間計算量** **平均空間計算量** **最悪空間計算量** **安定性**
**バブルソート** O(n²) O(n) O(1)
**選択ソート** O(n²) O(1)
**挿入ソート** O(n²) O(n) O(1)
**クイックソート** O(nlogn) O(n²) 平均と同じ O(logn) O(n)
**ヒープソート** O(nlogn) O(1)
**マージソート** O(nlogn) O(n)
**シェルソート** O(nlogn) O(n²) 平均と同じ O(1)
**カウントソート** O(n+k) O(n+k)
**基数ソート** O(N\*M) O(M)

1 マージソートは手回しアルゴリズムを使用して空間計算量をO(1)に下げることができますが、時間計算量が上がります。

2 基数ソートの時間計算量はO(N*M)です。Nはデータの個数、Mはデータの桁数です。

1.1 計算量の覚え方

  1. バブル、選択、挿入ソートは2つのforループを必要とし、各要素に注目するため、平均時間計算量はO(n²)です(要素を探すO(n)と位置を探すO(n))。
  2. クイック、マージ、シェル、ヒープは二分法の思想に基づいており、対数の底が2であるため、平均時間計算量はO(nlogn)です(要素を探すO(n)と位置を探すO(logn))。

1.2 安定性の覚え方

  • 安定性覚え方 -「クイシセイヒ」(クイックソートは安定性を犠牲にする)
  • ソートアルゴリズムの安定性:ソート前後で同じ要素の相対位置が変わらない場合、ソートアルゴリズムは安定しています。そうでない場合は不安定です。

二、時間計算量の理解

2.1 定数階O(1)

int x = 1;
int y = 2;
++x;
y++;
int z = x + y;

2.2 対数階O(logN)

int i = 1;
while(i < n)
{
    i = i * 2;
}

2.3 線形階O(n)

for(i = 0; i <= n; i++)
{
   System.out.println("こんにちは");
}

2.4 線形対数階O(nlogN)

for(m = 1; m < n; m++)
{
    i = 1;
    while(i < n)
    {
        i = i * 2;
    }
}

2.5 平方階O(n^2)

for(x = 1; x <= n; x++)
{
   for(i = 1; i <= n; i++)
    {
       System.out.println("こんにちは");
    }
}

2.6 K乗階O(n^k)

    for(i = 0; i <= n; i++)
        {
            for(j = 0; j <= n; j++)
            {
                for(k = 0; k <= n; k++)
                {
                    System.out.println("こんにちは");
                }
            }
        }


// k = 3 の場合、n ^ 3

上から下に時間計算量が大きくなるにつれて、実行効率は低下します。

三、空間計算量

3.1 定数階O(1) —— インプレースソート

tempという補助領域のみを使用

インプレースソートアルゴリズムとは、空間計算量がO(1)のアルゴリズムで、他の追加領域を必要としません~

    private static void swap(int[] values, int a, int b) {
        int temp = values[a];
        values[a] = values[b];
        values[b] = temp;
    }

3.2 対数階O(logN)

3.3 線形階O(n)

        int[] newArray = new int[values.length];
        for (int i = 0; i < values.length; i++) {
            newArray[i] = values[i];
        }

四、ソートアルゴリズム

4.1 バブルソート

(考え方:大きい要素を後方へ移動)

4.1.1 コード

    public static void bubbleSort(int[] arr) {
        // 最初のforはラウンドで、比較には参加しません
        for (int i = 0; i < arr.length - 1; i++) {
            // 2つ目のforで要素を1つずつ比較
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }
    }

4.1.2 計算量

時間計算量: N^2

空間計算量:1

最良時間計算度:N^2(内部ループで要素を比較しても交換しなくてもNと同じです)

安定性:安定しています(大きい場合のみ交換、小さいまたは等しい場合は交換しない)

    // { 0,1,2,3,4}
    private static void bubbleSort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            boolean isChange = false;
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                    isChange = true;
                }
            }
            if(!isChange){
                return;
            }
        }
    }

改良後のコード、最良時間計算量: N(最初のラウンドで要素の交換がなければ、すぐに退出できます。つまり、外側のループが1回のみです)

4.2 選択ソート

(考え方:最小の要素を最前方に配置)

4.2.1 コード

private static void selectSort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            int minIndex = i;
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            swap(nums, minIndex, i);
        }
    }

4.2.2 計算量

時間計算量: N^2

空間計算量:1

最良時間計算量:N^2

安定性:不安定

4.3 挿入ソート

(考え方:ソート済みの配列に適切な位置を挿入)

4.3.1 コード

挿入位置の検索 + 移動、2ステップで実行

public static void insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            // 挿入する要素を準備
            int insertNum = arr[i];
            int insertIndex = i;
            // 1つずつ比較して、挿入位置を見つける
            for (int j = i - 1; j >= 0; j--) {
                if(insertNum >= arr[j]){
                    break;
                }
                insertIndex = j;
            }

            // 1つずつ後方に移動、以下は上記forループに最適化できます
            if(insertIndex != i){
                for (int j = i; j > insertIndex ; j--) {
                    arr[j] = arr[j-1];
                }
                arr[insertIndex] = insertNum;
            }
        }
    }

簡潔バージョン~

private static void insertSort(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            int temp = nums[i];
            int j = i - 1;
            for (; j >= 0 && temp < nums[j]; j--) {
                nums[j + 1] = nums[j];
            }
            nums[j + 1] = temp;
        }
    }

ラウンド判断の考え方バージョン~

  private static void insertSort(int[] array) {
        // length - 1 回の比較、各ラウンドで次の要素をソート済みのシーケンスに挿入します。
        for (int i = 0; i < array.length - 1; i++) {
            int j = i + 1;
            int temp = array[j];
            for (; j > 0 && temp < array[j - 1]; j--) {
                // tempより大きい要素を右に移動します。
                array[j] = array[j - 1];
            }
            array[j] = temp;
        }
    }

4.3.2 計算量

時間計算量: N^2

空間計算量:1

最良時間計算量:N(内部ループに入らない場合。[1,2,3,4,5])

安定性:安定しています

4.4 クイックソート

(考え方:数値targetを使用して配列を2つに分割し、左側がtargetより大きく、右側がtargetより小さくする)

4.4.1 コード

/**
 * クイックソートアルゴリズム
 * @param nums ソート対象の配列
 * @param beginIndex ソート開始インデックス
 * @param endIndex ソート終了インデックス
 */
private static void quickSort(int[] nums, int beginIndex, int endIndex) {
    if (beginIndex >= endIndex) {
        return; // 再帰終了条件:開始インデックスが終了インデックス以上の場合、ソート完了
    }
    int mid = getMid(nums, beginIndex, endIndex); // 中間インデックスを取得して配列を分割
    quickSort(nums, beginIndex, mid - 1); // 中間インデックス左側の配列をクイックソート
    quickSort(nums, mid + 1, endIndex); // 中間インデックス右側の配列をクイックソート
}

/**
 * パーティションの中間要素のインデックスを取得
 * @param nums ソート対象の配列
 * @param beginIndex パーティションの開始インデックス
 * @param endIndex パーティションの終了インデックス
 * @return 中間要素のインデックス
 */
private static int getMid(int[] nums, int beginIndex, int endIndex) {
    int target = nums[beginIndex]; // 配列の先頭要素を基準値として使用
    int left = beginIndex;
    int right = endIndex;
    boolean right2left = true; // 検索方向を示すフラグ、trueの場合は右から左へ検索
    while (right > left) {
        if (right2left) {
            while (right > left && nums[right] > target) {
                right--;
            }
            if (right > left) {
                nums[left] = nums[right]; // 右側の要素が大きい場合、右側の要素を挿入位置に移動
                right2left = false; // 左から右へ検索に切り替え
            }
        } else {
            while (right > left && nums[left] < target) {
                left++;
            }
            if (right > left) {
                nums[right] = nums[left]; // 左側の要素が小さい場合、左側の要素を挿入位置に移動
                right2left = true; // 右から左へ検索に切り替え
            }
        }
    }
    nums[left] = target; // 基準値を挿入位置に配置して1回の交換を完了
    return left;
}

4.4.2 計算量

時間計算量: N Log N (各要素が中間位置を見つけるのにLogN時間かかり、N個の要素はNLogN)

最悪時間計算量:N ^ 2 (例:昇順配列[1,2,3,4,5])

空間計算量:Log N (再帰呼び出しにスタック領域が必要)

最悪空間計算量:N

空間計算量の分析:

通常の場合: o(logN)
            xxxxx
             / \
            /   \
           /     \
          /       \
         xx        xx
        / \        / \
       /   \      /   \
      /     \    /     \   
     x      ()  x       ()

最悪の場合は:o(n)

              xxxxx
               / \
              /   \
             /     \
            /       \
          xxxx      ()
          / \ 
         /   \
        /     \
      xxx     ()
      / \ 
     /   \ 
    xx    ()
   / \ 
  /   \ 
 x    ()

再帰木の深さはlogNで、各レベルの再帰にO(n)の時間がかかるため、総時間計算量はO(nlogn)です。

しかし、**極端な場合(例:昇順配列[1,2,3,4])**では、パーティションが常に極端に不均衡な分割を生成する場合、再帰木は非常に深くなります。

極端な場合を避け、時間計算量を常にN log(N)に保つために、ランダム化処理を行い、最初の要素を後続のランダムな要素と交換することができます。

import java.util.Random;

private static void quickSort(int[] nums, int beginIndex, int endIndex) {
    if (beginIndex >= endIndex) {
        return;
    }

    // ソート前にランダムに要素を選択して先頭要素と交換
    randomize(nums, beginIndex, endIndex);

    int mid = getMid(nums, beginIndex, endIndex);
    quickSort(nums, beginIndex, mid - 1);
    quickSort(nums, mid + 1, endIndex);
}

// ランダム化処理
private static void randomize(int[] nums, int beginIndex, int endIndex) {
    Random rand = new Random();
    int randomIndex = beginIndex + rand.nextInt(endIndex - beginIndex + 1);
    swap(nums, beginIndex, randomIndex);
}

// 配列内の2つの要素を交換
private static void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

安定性:不安定

4.4.3 関連面接問題

1、奇数と偶数の分割

配列を与え、奇数を左側に、偶数を右側に配置してください。ソートは不要です。

public static void main(String[] args) {
        // 問題:奇数を左側に、偶数を右側に配置
        // 考え方:クイックソート、要素を交換
        int[] array = {13, 100, 17, 12, 25, 0,1,6};
        quickSort(array, 0, array.length - 1);
        System.out.println(JSON.toJSONString(array));

    }

    private static void quickSort(int[] array, int beginIndex, int endIndex) {
        if (beginIndex >= endIndex) {
            return;
        }
        int mid = getMid(array, beginIndex, endIndex);
    }

    private static int getMid(int[] array, int beginIndex, int endIndex) {
        int left = beginIndex;
        int right = endIndex;
        int temp = array[beginIndex];

        boolean right2Left = true;
        while (left < right) {
            if (right2Left) {
                // 偶数の場合
                if (array[right] % 2 == 0) {
                    right--;
                } else {
                    array[left++] = array[right];
                    right2Left = false;
                }
            } else {
                // 奇数の場合
                if (array[left] % 2 != 0) {
                    left++;
                } else {
                    array[right--] = array[left];
                    right2Left = true;
                }
            }
        }
        array[left] = temp;
        return left;
    }

この方法の時間計算量はO(N)、空間計算量は O(1)、不安定です。

以下の方法の時間計算量はO(N)、空間計算量は O(N)、安定です。

public static void main(String[] args) {
        // 問題:奇数を左側に、偶数を右側に配置
        // 考え方:偶数の個数を見つけ、奇数は0から、偶数はoddCountから要素を配置(追加のint配列の補助領域が必要なので空間計算度はn)
        int[] array = {13, 100, 17, 12, 25, 0, 1, 6, 5, 5};

        int[] arrayNew = new int[array.length];

        int oddCount = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 != 0) {
                oddCount++;
            }
        }
        System.out.println(oddCount);
        // 奇数のインデックス
        int oddIndex = 0;
        // 偶数のインデックス
        int evenIndex = oddCount;
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 != 0) {
                arrayNew[oddIndex++] = array[i];
            }else {
                arrayNew[evenIndex++] = array[i];
            }

        }
        System.out.println(JSON.toJSONString(arrayNew));
    }

4.5 ヒープソート

(考え方:最大値を上に配置し、最後の要素と交換し、続いてヒープを構築)

4.5.1 コード

/**
 * ヒープソートアルゴリズム
 * @param nums ソート対象の配列
 * @param beginIndex ソート開始インデックス
 * @param endIndex ソート終了インデックス
 */
private static void heapSort(int[] nums, int beginIndex, int endIndex) {
    if (beginIndex >= endIndex) {
        return; // 開始インデックスが終了インデックス以上の場合、ソート完了
    }
    for (int i = endIndex; i >= beginIndex; i--) {
        createHeap(nums, i); // 最大ヒープを構築
        swap(nums, 0, i); // 最大要素を配列の末尾に移動
    }
}

/**
 * 最大ヒープを構築
 * @param nums 構築対象の配列
 * @param endIndex 現在のヒープの終了インデックス
 */
private static void createHeap(int[] nums, int endIndex) {
    int lastFatherIndex = (endIndex - 1) / 2;
    for (int i = lastFatherIndex; i >= 0; i--) {
        int biggestIndex = i;
        int leftChildIndex = i * 2 + 1;
        int rightChildIndex = i * 2 + 2;

        // 左子ノードが存在し、左子ノードの値が大きい場合
        if (leftChildIndex <= endIndex && nums[biggestIndex] < nums[leftChildIndex]) {
            biggestIndex = leftChildIndex;
        }
        // 右子ノードが存在し、右子ノードの値が大きい場合
        if (rightChildIndex <= endIndex && nums[biggestIndex] < nums[rightChildIndex]) {
            biggestIndex = rightChildIndex;
        }
        swap(nums, biggestIndex, i); // ヒープを調整して最大要素をヒープの頂部に配置
    }
}

/**
 * 配列内の2つの要素の位置を交換
 * @param nums 配列
 * @param i インデックス1
 * @param j インデックス2
 */
private static void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

4.5.2 計算量

時間計算量: N Log N (各要素を1回ヒープ化するのにLogN時間かかり、N個の要素はNLogN、どの場合も同じ)

空間計算量:1 (インプレースソート)

最悪時間計算量:N ^ 2 (例:昇順配列[1,2,3,4,5])

安定性:不安定

4.6 マージソート

再帰的な考え方:左右両方がソート済みなら、全体もソート済み

4.6.1 コード

// マージソートのメインメソッド
private static void mergeSort(int[] nums, int beginIndex, int endIndex) {
    // 開始インデックスが終了インデックス以上の場合、要素が1つまたは存在しないことを示し、ソート不要
    if (beginIndex >= endIndex) {
        return;
    }
    
    // 配列の中間インデックスを計算
    int mid = beginIndex + (endIndex - beginIndex) / 2;
    
    // 左半分を再帰的にソート
    mergeSort(nums, beginIndex, mid);
    
    // 右半分を再帰的にソート
    mergeSort(nums, mid + 1, endIndex);
    
    // 左右両半分をマージ
    merge(nums, beginIndex, mid, endIndex);
}

// マージ関数、左右両半分を1つのソート済み配列にマージするために使用
private static void merge(int[] nums, int beginIndex, int mid, int endIndex) {
    int left = beginIndex;
    int right = mid + 1;
    int[] newArrays = new int[endIndex - beginIndex + 1];
    int newArraysIndex = 0;

    // 左右両半分の要素を比較し、小さい要素を新しい配列に格納
    while (left <= mid && right <= endIndex) {
        newArrays[newArraysIndex++] = nums[left] <= nums[right] ? nums[left++] : nums[right++];
    }

    // 残りの左半分の要素を新しい配列にコピー
    while (left <= mid) {
        newArrays[newArraysIndex++] = nums[left++];
    }

    // 残りの右半分の要素を新しい配列にコピー
    while (right <= endIndex) {
        newArrays[newArraysIndex++] = nums[right++];
    }

    // マージされた新しい配列を元の配列にコピー
    for (int i = 0; i < newArrays.length; i++) {
        nums[beginIndex + i] = newArrays[i];
    }
}

4.6.2 計算量

時間計算量: N Log N (各要素を再帰的に処理するのにLogN時間かかり、N個の要素はNLogN、どの場合も同じ)

空間計算量:N

安定性:安定しています

4.7 シェルソート

考え方:挿入ソートのアップグレード版(セグメント式挿入ソート)

4.7.1 コード

private static void quickSort(int[] nums) {
//        int gap = nums.length / 2;
//        while (gap > 0) {
        for (int i = 1; i < nums.length; i++) {
            int temp = nums[i];
            int j;
            for (j = i - 1; j >= 0 && temp < nums[j]; j--) {
                nums[j + 1] = nums[j];
            }
            nums[j + 1] = temp;
        }
//        gap = gap / 2;
//        }
    }

    // 上記のクイックソートをシェルソートに変更するには、間隔1をgapに変更するだけ
    private static void shellSort(int[] nums) {
        int gap = nums.length / 2;
        while (gap > 0) {
            for (int i = gap; i < nums.length; i++) {
                int temp = nums[i];
                int j;
                for (j = i - gap; j >= 0 && temp < nums[j]; j = j - gap) {
                    nums[j + gap] = nums[j];// 現在の要素が挿入要素より大きい場合、現在の要素を後方に移動
                }
                nums[j + gap] = temp; // 上記でj=j-gapで退出した場合、jは1回減らされている可能性があり、0未満になることがある
            }
            gap = gap / 2;
        }
    }

4.7.2 計算量

時間計算量: N Log N

空間計算量:1

安定性:不安定

4.8 カウントソート

前提条件:

1、配列の规模が大きくない、例えば中国の各年齢層の人数を統計(年齢は最大で0-150歳)

4.8.2 コード

public static void main(String[] args) {
    int[] nums = {10, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 2, 8, 3, 4, 1, 2, 3, 4};
    int[] ageCount = new int[200];
    for (int i = 0; i < nums.length; i++) {
        ageCount[nums[i]]++;
    }
    for (int i = 1; i < ageCount.length; i++) {
        int count = ageCount[i];
        for (int j = 0; j < count; j++) {
            System.out.print(i + ",");
        }
    }
}

4.8.2 計算量

時間計算量: N (Nは配列の長さ)

空間計算量:K

安定性:安定しています

4.9 基数ソート

4.9.1 コード

指定された位置の数字を取得する方法は?

    public static void main(String[] args) {
        int number = 1234;
        // 1234から一の位の4を取得する方法:1234 % 10 = 4 、4/1 = 4
        // 1234から十の位の3を取得する方法:1234 % 100 = 34 、34/10 = 3
        // 1234から百の位の2を取得する方法:1234 % 1000 = 234 、234/100 = 2
        // 1234から千の位の1を取得する方法:1234 % 10000 = 1234 、1234/1000 = 1
        for (int i = 0; i < getDigitLength(number); i++) {
            int num = (int) (number % Math.pow(10, i + 1) / Math.pow(10, i));
            System.out.println(num);
        }
    }

    // 1は1桁
    // 10は2桁
    // 100は3桁
    // 1000は4桁
    public static int getDigitLength(int number) {
        int length = 1;
        while (number / 10 > 0) { // 100
            length++;
            number = number / 10;
        }
        return length;
    }

基数ソートアルゴリズム:

public static void main(String[] args) {

        int[] array = {13, 100, 17, 12, 25};
        Map<Integer, List<Integer>> bucketMap = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            bucketMap.put(i, new ArrayList<>());
        }

        int maxDigitLength = 0;
        for (int i = 0; i < array.length; i++) {
            maxDigitLength = getDigitLength(array[i]) > maxDigitLength ? getDigitLength(array[i]) : maxDigitLength;
        }
        for (int i = 0; i < maxDigitLength; i++) { // 後ろから前へ数値を取得
            for (int j = 0; j < array.length; j++) {
                int iValue = (int) (array[j] % Math.pow(10, i + 1) / Math.pow(10, i));
                bucketMap.get(iValue).add(array[j]); // バケットに入れる
            }

            int arrayIndex = 0;
            for (int j = 0; j < 10; j++) {
                List<Integer> bucketList = bucketMap.get(j);
                for (Integer num: bucketList) {
                    array[arrayIndex++] = num; // 倒し出す
                }
                map.get(j).clear(); // バケットをクリア
            }
            System.out.println(JSON.toJSONString(array));

        }
    }


    // 1は1桁
    // 10は2桁
    // 100は3桁
    // 1000は4桁
    public static int getDigitLength(int number) {
        int length = 1;
        while (number / 10 > 0) { // 100
            length++;
            number = number / 10;
        }
        return length;
    }

基数ソートアルゴリズム:(別途手書きで実装)

 public static void radixSort(int[] array) {
        if (array == null || array.length < 2) {
            return; // 配列がソート不要かどうかを確認
        }

        int maxDigitLength = 0;
        // 最大桁数を計算
        for (int i = 0; i < array.length; i++) {
            maxDigitLength = Math.max(maxDigitLength, getDigitLength(array[i]));
        }

        List<List<Integer>> bucketList = Lists.newArrayList();
        for (int i = 0; i < 10; i++) {
            bucketList.add(Lists.newArrayList());
        }
        // 各桁に基づいて分配と収集を実行
        // 例えば最大の数字が1234の場合、最長は4桁で、つまり4回の入れ出しで完了
        for (int i = 0; i < maxDigitLength; i++) {
            // 要素をバケットに分配
            for (int j = 0; j < array.length; j++) {
                // 指定されたインデックスに基づいて対応する数字を取得
                int bucketIndex = getBucketIndex(array[j], i);
                bucketList.get(bucketIndex).add(array[j]);
            }
            System.out.println("i -> " + i + ",\n ソート後" + JSON.toJSONString(bucketList));
            // 倒し出す
            int newIndex = 0;
            for (List<Integer> bucketInnerNumberList: bucketList) {
                for (Integer number: bucketInnerNumberList) {
                    array[newIndex++] = number;
                }
                // 配列をクリア
                bucketInnerNumberList.clear();
            }
        }
    }

    /**
     * 指定されたインデックスに基づいて対応する数字を取得
     * @param number     1234
     * @param digitIndex 0 の場合は 4 を返し、1 の場合は 3 を返し、2 の場合は 2 を返し、3 の場合は 1 を返す
     */
    private static int getBucketIndex(int number, int digitIndex) {
        // 1234
        // 0 の場合は 4 を返す
        // 1 の場合は 3 を返す
        // 2 の場合は 2 を返す
        // 3 の場合は 1 を返す
        int result = 0;
        for (int i = 0; i <= digitIndex; i++) {
            result = number % 10;
            number = number / 10;
        }
        return result;
    }

    /**
     * 数字の桁数を取得
     */
    public static int getDigitLength(int number) {
        // 1234 -> 4
        // 123 -> 3
        // 12 -> 2
        // 1 -> 1
        int result = 1;
        while (number / 10 != 0) {
            result++;
            number /= 10;
        }
        return result;
    }

4.9.2 計算量

時間計算量: N * d (Nは配列の長さ、dは最大桁数、整数ソートの場合、d = log N 程度、全体として N Log N)

空間計算量:N

安定性:安定しています

4.9.3 使用シーン

基数ソートは比較を必要としない整数ソートアルゴリズムで、その動作原理は下位の桁からソートし、収集してから、上位の桁からソートし、再び収集する、というものです。場合によっては逆の順序、つまり上位の桁からソートすることもあります。これは安定したソートアルゴリズムを使用して各桁をソートする基数ソートアルゴリズムです。

基数ソートの使用シーンは以下の通りです:

  1. 大量データ:データ量が多く、データの範囲がそれほど大きくない場合、基数ソートは非常に効果的です。
  2. 固定長データ:基数ソートは固定長のデータソートに非常に効率的です。例えば、電話番号、身分証明番号などです。
  3. 整数と文字列のソート:基数ソートは整数ソートに適していますが、データ型が順序のある要素セグメントに分解できる場合、文字列または他のデータ型にも使用できます。例えば、ASCII値によってです。
  4. 安定ソートの必要性:基数ソートは安定しているため、同じ要素の元の順序を維持する必要がある場合に非常に有用です。

カウントソートと比較した場合、主な違いは以下の通りです:

  1. 処理するデータ型:カウントソートは整数ソートにのみ使用でき、小さい範囲の整数に適しています。基数ソートは桁に分解できる任意のデータ型に使用できます。整数と文字列を含みます。
  2. 効率性:小さい範囲の整数の場合、カウントソートは基数ソートより効率的な場合があります。なぜなら、データを1回走査するだけでソートを完了できるからです。しかし、大きい範囲または多桁のデータの場合、基数ソートは通常より優れています。なぜなら、より大きい範囲の値を線形時間で処理できるからです。
  3. メモリ使用量:カウントソートは大きい範囲の値を処理する場合に多くのメモリを消費する可能性があります。なぜなら、各値の出現回数を格納するのに十分な大きさのカウント配列が必要だからです。基数ソートはデータを逐次的に処理することでこの問題を回避します。
  4. 安定性:カウントソート自体は安定していますが、基数ソートは全体の安定性を保証するために安定したソートアルゴリズムをサブプロセスとして使用する必要があります。

総じて、基数ソートは高速で安定したソート方法であり、大量データと多桁データのソートに適しています。しかし、メモリの需要は相対的に高く、複雑であるため、通常は特定の条件下で使用する場合に最も効率的です。

基数ソートを使用して大量の携帯電話番号を処理する場合、時間計算量はいくつになりますか?

基数ソートの時間計算量は通常O(kN)と表現されます。ここで、Nはソート要素の数(この場合は携帯電話番号の数)、kは要素の統計的特性で、通常はこれらの要素の最大桁数に依存します。

携帯電話番号の場合、国の標準的な携帯電話番号の長さを考慮すると、通常この長さは固定されています。例えば、ある国の携帯電話番号が10桁である場合、kは定数、つまりk = 10と考えることができます。

この場合、kは固定値であるため、基数ソートがこれらの携帯電話番号を処理する時間計算量は**O(N)**に簡略化できます。つまり、線形時間でソートを完了できます。これは、内部で使用される安定したソートアルゴリズム(例えばカウントソート)も線形であると仮定した場合です。

しかし、実際の時間計算量は他の要因の影響も受けます。例えば、使用される安定したソートアルゴリズムの複雑さ、携帯電話番号の分布状況、実際の操作でのバケット(またはカウント配列)の処理効率などです。

タグ: ソートアルゴリズム 時間計算量 空間計算量

7月28日 04:57 投稿