排序算法学习

一、引言

排序算法在计算机科学中占据着至关重要的地位。无论是处理数据、搜索算法还是优化程序性能,都离不开高效的排序。不同的排序算法有着各自的特点和适用场景,了解它们的原理、时间和空间复杂度以及代码实现,有助于我们在实际编程中选择最合适的算法来解决问题。

二、常见排序算法详解

(一)冒泡排序(Bubble Sort)

  1. 原理
    冒泡排序是一种简单的比较排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。每一轮遍历都会将一个最大(或最小)的元素 "冒泡" 到数列的一端。

  2. 时间复杂度

    • 最坏情况:O(n²)‌。当数列是逆序排列时,每一次比较都需要进行交换,需要进行n(n-1)/2次比较和交换操作。
    • 最好情况:O(n)‌。当数列已经是有序的,只需要进行一轮遍历,没有交换操作。
  3. 空间复杂度:**O(1)**‌。只需要在交换元素时使用少量的额外空间。

  4. 优点

    • 实现简单,代码容易理解和编写。
    • 适用于小规模数据或对算法效率要求不高的场景。
  5. 缺点

    • 效率较低,尤其是对于大规模数据排序时,时间消耗大。
  6. C# 实现

    class BubbleSort
    {
    public static void Sort(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])
    {
    int temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    }
    }

  7. Python 实现

    def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
    for j in range(0, n - i - 1):
    if arr[j] > arr[j + 1]:
    arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

  8. 应用场景

    • 适用于对简单、少量数据进行排序且对效率要求不高的情况。例如,在教学示例中展示排序的基本原理。

(二)选择排序(Selection Sort)

  1. 原理
    每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

  2. 时间复杂度

    • 无论数据初始状态如何,时间复杂度都是O(n²)‌。需要进行n(n-1)/2次比较来找到最小(或最大)元素。
  3. 空间复杂度:O(1)‌。只需要常数级别的额外空间。

  4. 优点

    • 实现简单,和冒泡排序类似,代码容易理解。
    • 移动数据次数较少,相比冒泡排序在某些情况下略有优势。
  5. 缺点

    • 总体效率不高,比较次数多。
  6. C# 实现

    class SelectionSort
    {
    public static void Sort(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[j] < arr[minIndex])
    {
    minIndex = j;
    }
    }
    if (minIndex!= i)
    {
    int temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    }
    }
    }
    }

  7. Python 实现

    def selection_sort(arr):
    n = len(arr)
    for i in range(n):
    min_index = i
    for j in range(i + 1, n):
    if arr[j] < arr[min_index]:
    min_index = j
    arr[i], arr[min_index] = arr[min_index], arr[i]
    return arr

  8. 应用场景

    • 适用于数据量较小且对稳定性要求不高的场景。在一些简单的数据处理任务中可以使用。

(三)插入排序(Insertion Sort)

  1. 原理
    将未排序的数据插入到已经排序的序列合适位置中,就像打牌时整理手牌一样。

  2. 时间复杂度

    • 最坏情况:O(n²)‌。当数据是逆序排列时,每次插入都需要比较和移动多个元素。
    • 最好情况:O(n)‌。当数据已经是有序的,只需遍历一次,无需移动元素。
  3. 空间复杂度:O(1)‌

  4. 优点

    • 对于接近有序的数据,效率较高。
    • 实现简单,在小规模数据下表现较好。
  5. 缺点

    • 数据量较大且无序时,效率较低。
  6. C# 实现

    class InsertionSort
    {
    public static void Sort(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;
    }
    }
    }

  7. Python 实现

    def insertion_sort(arr):
    for i in range(1, len(arr)):
    key = arr[i]
    j = i - 1
    while j >= 0 and arr[j] > key:
    arr[j + 1] = arr[j]
    j -= 1
    arr[j + 1] = key
    return arr

  8. 应用场景

    • 常用于对小规模数据进行排序,或者部分数据已经有序的情况下。例如,在处理实时产生的、增量式的数据时可能会用到。

(四)快速排序(Quick Sort)

  1. 原理
    通过选择一个基准值,将数列分为两部分,小于基准值的元素放在左边,大于基准值的元素放在右边,然后递归地对左右两部分进行排序。

  2. 时间复杂度

    • 最坏情况:O(n²)‌。当选择的基准值每次都是最大值或最小值时,划分不均匀,导致递归树深度为n,时间复杂度退化为O(n²)‌。
    • 平均情况和最好情况:O(n log n)‌。在随机选择基准值且数据分布较为均匀的情况下,可以达到这个复杂度。
  3. 空间复杂度

    • 平均情况:O(log n)‌,递归过程中需要使用栈空间来保存状态。
    • 最坏情况:O(n)‌
  4. 优点

    • 速度快,在大多数情况下效率很高。
    • 是原地排序算法,不需要额外的大量空间。
  5. 缺点

    • 不稳定,相同元素的相对顺序可能会改变。
    • 最坏情况时间复杂度较高。
  6. C# 实现

    class QuickSort
    {
    public static void Sort(int[] arr, int left, int right)
    {
    if (left < right)
    {
    int pivotIndex = Partition(arr, left, right);
    Sort(arr, left, pivotIndex - 1);
    Sort(arr, pivotIndex + 1, right);
    }
    }

     private static int Partition(int[] arr, int left, int right)
     {
         int pivot = arr[right];
         int i = left - 1;
         for (int j = left; j < right; j++)
         {
             if (arr[j] < pivot)
             {
                 i++;
                 int temp = arr[i];
                 arr[i] = arr[j];
                 arr[j] = temp;
             }
         }
         int temp2 = arr[i + 1];
         arr[i + 1] = arr[right];
         arr[right] = temp2;
         return i + 1;
     }
    

    }

  7. Python 实现

    def quick_sort(arr):
    if len(arr) <= 1:
    return arr
    else:
    pivot = arr[0]
    left = [x for x in arr[1:] if x < pivot]
    right = [x for x in arr[1:] if x >= pivot]
    return quick_sort(left) + [pivot] + quick_sort(right)

  8. 应用场景

    • 适用于大规模数据排序,在很多标准库的排序函数中常被使用。

(五)归并排序(Merge Sort)

  1. 原理
    将数列不断地分割成两半,直到每个子数列只有一个元素,然后将分割后的子数列合并成有序数列。

  2. 时间复杂度:O(n log n)‌。无论数据初始状态如何,都能保持这个复杂度,因为分割和合并的操作次数与数据规模的对数相关。

  3. 空间复杂度:O(n)‌。在合并过程中需要额外的空间来存储临时数据。

  4. 优点

    • 稳定的排序算法,相同元素的相对顺序在排序后不会改变。
    • 适用于大规模数据排序,时间复杂度稳定。
  5. 缺点

    • 需要额外的空间,相比一些原地排序算法可能消耗更多内存。
  6. C# 实现

    class MergeSort
    {
    public static void Sort(int[] arr)
    {
    MergeSortRecursive(arr, 0, arr.Length - 1);
    }

     private static void MergeSortRecursive(int[] arr, int left, int right)
     {
         if (left < right)
         {
             int mid = (left + right) / 2;
             MergeSortRecursive(arr, left, mid);
             MergeSortRecursive(arr, mid + 1, right);
             Merge(arr, left, mid, right);
         }
     }
    
     private static void Merge(int[] arr, int left, int mid, int right)
     {
         int n1 = mid - left + 1;
         int n2 = right - mid;
    
         int[] leftArray = new int[n1];
         int[] rightArray = new int[n2];
    
         Array.Copy(arr, left, leftArray, 0, n1);
         Array.Copy(arr, mid + 1, rightArray, 0, n2);
    
         int i = 0, j = 0, k = left;
         while (i < n1 && j < n2)
         {
             if (leftArray[i] <= rightArray[j])
             {
                 arr[k] = leftArray[i];
                 i++;
             }
             else
             {
                 arr[k] = rightArray[j];
                 j++;
             }
             k++;
         }
    
         while (i < n1)
         {
             arr[k] = leftArray[i];
             i++;
             k++;
         }
    
         while (j < n2)
         {
             arr[k] = rightArray[j];
             j++;
             k++;
         }
     }
    

    }

  7. Python 实现

    def merge_sort(arr):
    if len(arr) <= 1:
    return arr
    mid = len(arr) // 2
    left_half = merge_sort(arr[:mid])
    right_half = merge_sort(arr[mid:])
    return merge(left_half, right_half)

    def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
    if left[i] < right[j]:
    result.append(left[i])
    i += 1
    else:
    result.append(right[j])
    j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

  8. 应用场景

    • 常用于对数据稳定性有要求的大规模数据排序,如数据库系统中的排序操作。

(六)堆排序(Heap Sort)

  1. 原理
    利用堆这种数据结构来进行排序。首先将待排序数列构建成一个大顶堆(或小顶堆),然后将堆顶元素与堆尾元素交换,再调整剩余元素为堆,重复这个过程直到数列有序。

  2. 时间复杂度:O(n log n)‌ 。建堆过程的时间复杂度为O(n)‌ ,每次调整堆的时间复杂度为O(log n)‌,共进行n次调整。

  3. 空间复杂度:O(1)‌。只需要在交换元素时使用少量额外空间。

  4. 优点

    • 时间复杂度稳定,效率较高。
    • 适用于大规模数据排序。
  5. 缺点

    • 实现相对复杂一些,理解难度较大。
  6. C# 实现

    class HeapSort
    {
    public static void Sort(int[] arr)
    {
    int n = arr.Length;

         // 构建大顶堆
         for (int i = n / 2 - 1; i >= 0; i--)
         {
             Heapify(arr, n, i);
         }
    
         // 排序
         for (int i = n - 1; i > 0; i--)
         {
             // 将堆顶元素与末尾元素交换
             int temp = arr[0];
             arr[0] = arr[i];
             arr[i] = temp;
    
             // 调整堆
             Heapify(arr, i, 0);
         }
     }
    
     private static void Heapify(int[] arr, int n, int i)
     {
         int largest = i;
         int left = 2 * i + 1;
         int right = 2 * i + 2;
    
         if (left < n && arr[left] > arr[largest])
         {
             largest = left;
         }
    
         if (right < n && arr[right] > arr[largest])
         {
             largest = right;
         }
    
         if (largest!= i)
         {
             int swap = arr[i];
             arr[i] = arr[largest];
             arr[largest] = swap;
    
             Heapify(arr, n, largest);
         }
     }
    

    }

  7. Python 实现

    def heap_sort(arr):
    def heapify(arr, n, i):
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2

         if left < n and arr[left] > arr[largest]:
             largest = left
         if right < n and arr[right] > arr[largest]:
             largest = right
    
         if largest!= i:
             arr[i], arr[largest] = arr[largest], arr[i]
             heapify(arr, n, largest)
    
     n = len(arr)
     # 构建大顶堆
     for i in range(n // 2 - 1, -1, -1):
         heapify(arr, n, i)
    
     # 排序
     for i in range(n - 1, 0, -1):
         arr[0], arr[i] = arr[i], arr[0]
         heapify(arr, i, 0)
    
     return arr
    
  8. 应用场景

    • 在需要高效排序且对内存空间使用有限制的情况下可以使用,例如一些嵌入式系统或者对性能要求较高的算法中。

(七)希尔排序(Shell Sort)

  1. 原理
    希尔排序是插入排序的一种改进版本。它先将待排序的数列分割成若干个较小的子序列,然后对每个子序列进行插入排序。随着分割的间隔逐渐减小,数列越来越接近有序,最后进行一次整体的插入排序。

  2. 时间复杂度

    • 希尔排序的时间复杂度较难准确计算,取决于间隔序列的选择。一般来说,平均时间复杂度为*:O(n^(1.3---2))*。
    • 最坏情况时间复杂度为O(n²)‌
  3. 空间复杂度:O(1)‌。只需要少量的额外空间用于临时变量。

  4. 优点

    • 相比插入排序,在大规模数据下效率更高。
    • 实现相对简单,代码容易理解。
  5. 缺点

    • 不稳定排序算法,相同元素的相对顺序可能会改变。
    • 时间复杂度的计算较为复杂,难以精确确定。
  6. C# 实现

    class ShellSort
    {
    public static void Sort(int[] arr)
    {
    int n = arr.Length;
    int gap = n / 2;
    while (gap > 0)
    {
    for (int i = gap; i < n; i++)
    {
    int temp = arr[i];
    int j = i;
    while (j >= gap && arr[j - gap] > temp)
    {
    arr[j] = arr[j - gap];
    j -= gap;
    }
    arr[j] = temp;
    }
    gap /= 2;
    }
    }
    }

  7. Python 实现

    def shell_sort(arr):
    n = len(arr)
    gap = n // 2
    while gap > 0:
    for i in range(gap, n):
    temp = arr[i]
    j = i
    while j >= gap and arr[j - gap] > temp:
    arr[j] = arr[j - gap]
    j -= gap
    arr[j] = temp
    gap //= 2
    return arr

  8. 应用场景

    • 在处理中等规模数据且对稳定性要求不高时表现较好。例如,对于一些初步整理数据的任务,希尔排序可以快速地使数据接近有序状态,为后续可能的进一步处理打下基础。

(八)计数排序(Counting Sort)

  1. 原理
    计数排序不是基于比较的排序算法。它通过统计每个元素在数列中出现的次数,然后根据元素的大小确定其在有序序列中的位置。具体做法是先找出数列中的最大值和最小值,创建一个计数数组,其长度为最大值与最小值的差值加 1,然后统计每个元素出现的次数存入计数数组,最后根据计数数组将元素依次放回原数组中得到有序序列。

  2. 时间复杂度

    • 最好、最坏和平均情况时间复杂度都是O(n+m)‌,其中n是待排序数组的元素个数,m是数组中元素的取值范围。m当相对较小时,n计数排序效率很高。
  3. 空间复杂度:O(n+m)‌。需要创建计数数组来存储元素的计数信息。

  4. 优点

    • 算法简单,在元素取值范围有限且集中时速度极快。
    • 是稳定的排序算法。
  5. 缺点

    • 当元素取值范围过大时,需要消耗大量的空间来创建计数数组,不适合。
  6. C# 实现

    class CountingSort
    {
    public static void Sort(int[] arr)
    {
    if (arr.Length == 0)
    return;

         int maxValue = arr.Max();
         int minValue = arr.Min();
         int[] countArray = new int[maxValue - minValue + 1];
    
         foreach (var num in arr)
         {
             countArray[num - minValue]++;
         }
    
         int index = 0;
         for (int i = 0; i < countArray.Length; i++)
         {
             while (countArray[i] > 0)
             {
                 arr[index] = i + minValue;
                 index++;
                 countArray[i]--;
             }
         }
     }
    

    }

  7. Python 实现

    def counting_sort(arr):
    if not arr:
    return arr
    max_value = max(arr)
    min_value = min(arr)
    count_array = [0] * (max_value - min_value + 1)
    for num in arr:
    count_array[num - min_value] += 1
    sorted_array = []
    for i, count in enumerate(count_array):
    for _ in range(count):
    sorted_array.append(i + min_value)
    return sorted_array

  8. 应用场景

    • 适用于对整数进行排序且整数的取值范围不是很大的情况。例如,在一些数据统计相关的任务中,如果需要对统计结果进行排序,计数排序可以快速完成。

(九)桶排序(Bucket Sort)

  1. 原理
    桶排序是一种分布式排序算法。它将数据划分到不同的桶(可以是链表或数组实现)中,每个桶再单独进行排序(通常可以使用其他排序算法或者递归地使用桶排序),最后将各个桶中的数据依次连接起来得到有序序列。划分桶的方式通常根据数据的范围和分布来确定。

  2. 时间复杂度

    • 平均情况下时间复杂度为O(n+m)‌,其中n是元素个数,m是桶的个数。在数据分布均匀的情况下效率较高。
    • 最坏情况时间复杂度可能接近O(n²)‌,如果数据都集中在一个桶中。
  3. 空间复杂度:O(n+m)‌。需要创建桶来存储数据以及一些额外的空间用于辅助排序。

  4. 优点

    • 对于数据分布均匀的情况效率很高。
    • 可以并行处理,提高排序速度。
  5. 缺点

    • 数据分布不均匀时性能下降明显。
    • 桶的划分和数据分配策略如果不合理会影响效率。
  6. C# 实现

    class BucketSort
    {
    public static void Sort(float[] arr, int bucketCount)
    {
    List<float>[] buckets = new List<float>[bucketCount];
    for (int i = 0; i < bucketCount; i++)
    {
    buckets[i] = new List<float>();
    }

         float maxValue = arr.Max();
         float minValue = arr.Min();
         float interval = (maxValue - minValue) / bucketCount;
    
         foreach (var num in arr)
         {
             int bucketIndex = (int)((num - minValue) / interval);
             if (bucketIndex == bucketCount)
                 bucketIndex--;
             buckets[bucketIndex].Add(num);
         }
    
         for (int i = 0; i < bucketCount; i++)
         {
             if (buckets[i].Count > 1)
                 buckets[i].Sort();
         }
    
         int index = 0;
         for (int i = 0; i < bucketCount; i++)
         {
             foreach (var num in buckets[i])
             {
                 arr[index] = num;
                 index++;
             }
         }
     }
    

    }

  7. Python 实现

    def bucket_sort(arr, bucket_size=5):
    min_value = min(arr)
    max_value = max(arr)
    bucket_count = (max_value - min_value) // bucket_size + 1
    buckets = [[] for _ in range(bucket_count)]
    for num in arr:
    bucket_index = (num - min_value) // bucket_size
    buckets[bucket_index].append(num)
    for bucket in buckets:
    bucket.sort()
    sorted_array = []
    for bucket in buckets:
    sorted_array.extend(bucket)
    return sorted_array

  8. 应用场景

    • 常用于对大量浮点数进行排序,当数据分布相对均匀时效果较好。例如,在处理大规模的成绩数据、概率分布数据等方面有应用。

(十)基数排序(Radix Sort)

  1. 原理
    基数排序是一种非比较型整数排序算法。它按照从低位到高位的顺序依次对待排序元素的每一位数字进行排序。通常使用计数排序或桶排序作为每一位数字排序的方法。通过多次这样的按位排序,最终使整个数列有序。

  2. 时间复杂度

    • 时间复杂度为O(g(n+m))‌,其中g是数字的位数,n是元素个数,m是基数(通常是 10 或进制数)。在数字位数固定且不大的情况下效率较高。
  3. 空间复杂度:O(n+m)‌。需要额外的空间来存储计数信息或进行桶的分配。

  4. 优点

    • 稳定性好,适用于整数排序。
    • 时间复杂度线性,在某些情况下速度快于基于比较的排序算法。
  5. 缺点

    • 只适用于整数类型数据排序,且对于位数较长的整数可能需要较多的排序轮次。
  6. C# 实现

    class RadixSort
    {
    public static void Sort(int[] arr)
    {
    int maxValue = arr.Max();
    int digitPlace = 1;
    while (maxValue / digitPlace > 0)
    {
    CountingSortForRadix(arr, digitPlace);
    digitPlace *= 10;
    }
    }

     private static void CountingSortForRadix(int[] arr, int digitPlace)
     {
         int[] countArray = new int[10];
         int[] outputArray = new int[arr.Length];
    
         for (int i = 0; i < arr.Length; i++)
         {
             int digit = (arr[i] / digitPlace) % 10;
             countArray[digit]++;
         }
    
         for (int i = 1; i < 10; i++)
         {
             countArray[i] += countArray[i - 1];
         }
    
         for (int i = arr.Length - 1; i >= 0; i--)
         {
             int digit = (arr[i] / digitPlace) % 10;
             outputArray[countArray[digit] - 1] = arr[i];
             countArray[digit]--;
         }
    
         for (int i = 0; i < arr.Length; i++)
         {
             arr[i] = outputArray[i];
         }
     }
    

    }

  7. Python 实现

    def radix_sort(arr):
    max_value = max(arr)
    digit_place = 1
    while max_value // digit_place > 0:
    buckets = [[] for _ in range(10)]
    for num in arr:
    digit = (num // digit_place) % 10
    buckets[digit].append(num)
    arr = [num for bucket in buckets for num in bucket]
    digit_place *= 10
    return arr

  8. 应用场景

    • 在处理大量整数数据,尤其是需要按照数字的位进行排序的情况,如排序身份证号码、邮政编码等场景中较为常用。

三、总结

不同的排序算法各自有着独特的原理、时间和空间复杂度特点以及应用场景。在实际应用中,我们需要根据数据的规模、特点、分布情况以及对算法效率、稳定性和空间占用的要求等因素综合考虑,选择最为合适的排序算法。通过深入理解这些排序算法,我们能够更加高效地处理数据排序问题,优化程序性能,满足各种不同的计算需求,为计算机科学领域的发展和实际应用提供有力的支持。无论是在日常的编程任务中,还是在大规模数据处理、数据库管理等专业领域,排序算法都发挥着不可或缺的作用。

相关推荐
一只码代码的章鱼7 分钟前
排序算法 (插入,选择,冒泡,希尔,快速,归并,堆排序)
数据结构·算法·排序算法
青い月の魔女26 分钟前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
我要出家当道士1 小时前
Nginx单向链表 ngx_list_t
数据结构·nginx·链表·c
林的快手1 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
千天夜1 小时前
多源多点路径规划:基于启发式动态生成树算法的实现
算法·机器学习·动态规划
从以前1 小时前
准备考试:解决大学入学考试问题
数据结构·python·算法
.Vcoistnt2 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
ALISHENGYA2 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
数据结构·c++·算法·图论
我码玄黄4 小时前
正则表达式优化之算法和效率优化
前端·javascript·算法·正则表达式
Solitudefire5 小时前
蓝桥杯刷题——day9
算法·蓝桥杯