28. 排序算法

一、冒泡排序

  冒泡排序(Bubble Sort)重复地遍历待排序的数列,依次比较相邻元素的值,如果它们的顺序错误(比如在升序排序中,前一个元素大于后一个元素),就交换它们的位置。遍历的过程会重复进行多次,直到整个数列变成有序状态。冒泡排序的名字来源于较小的元素会像水底的气泡一样逐渐 “浮” 到数列的顶端。

冒泡排序

/**
 * @brief 冒泡排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void BubbleSort(ElementType A[], int N)
{
    int i = 0, j = 0;
    int flag = 0;

    for (i = 0; i < N - 1; i++)                                                 // 循环次数为N-1
    {
        flag = 0;
        for (j = 0; j < N - i - 1; j++)                                         // 每次循环,最后一个元素已经有序,因此循环次数为n-i-1
        {
            if (A[j] > A[j + 1])
            {
                Swap(&A[j], &A[j + 1]);
                flag = 1;
            }
        }

        if (flag == 0)                                                          // 全程无交换,序列已经有序
        {
            break;
        }
    }
}
/**
 * @brief 交换两个数
 * 
 * @param a 数1
 * @param b 数2
 */
void Swap(ElementType * a, ElementType * b)
{
    ElementType temp = * a;
    * a = * b;
    * b = temp;
}

最好情况下,时间复杂度为 O(N),最好情况下,时间复杂度为 O(N2)

二、插入排序

  插入排序(Insertion Sort)的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序

/**
 * @brief 插入排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void InsertionSort(ElementType A[], int N)
{
    int i = 0, j = 0;
    ElementType temp = 0;

    for (i = 1; i < N; i++)                                                     // 循环次数为N-1
    {
        temp = A[i];                                                            // 待插入元素
        for (j = i; j > 0 && A[j - 1] > temp; j--)                              // 寻找插入位置
        {
            A[j] = A[j - 1];                                                    // 元素后移
        }
        A[j] = temp;                                                            // 插入元素
    }
}

最好情况下,时间复杂度为 O(N),最好情况下,时间复杂度为 O(N2)

三、希尔排序

  希尔排序(Shell Sort)是基于插入排序的一种更高效的改进版本,它通过将待排序的元素按照一定的间隔分组,对每组使用直接插入排序,随着间隔逐渐缩小,直至间隔为 1,整个序列就是一个组,此时再进行一次插入排序,整个序列就完成了排序。

希尔排序

/**
 * @brief 希尔排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void ShellSort(ElementType A[], int N)
{
    int i = 0, j = 0;
    ElementType temp = 0;

    for (int D = N / 2; D >= 1; D /= 2)                                         // 希尔增量序列
    {
        // 插入排序
        for (i = D; i < N; i++)                                                 // 循环次数为N-1
        {
            temp = A[i];                                                        // 待插入元素
            for (j = i; j >= D && A[j - D] > temp; j -= D)                      // 寻找插入位置
            {
                A[j] = A[j - D];                                                // 元素后移
            }
            A[j] = temp;                                                        // 插入元素
        }
    }
}

四、快速排序

  快速排序(Quick Sort)的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序

/**
 * @brief 快速排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void QuickSort(ElementType A[], int N)
{
    Quick(A, 0, N - 1);
}
/**
 * @brief 获取主元
 * 
 * @param A 数组
 * @param left 排序区间的左边界
 * @param right 排序区间的右边界
 */
ElementType Median(ElementType A[], int left, int right)
{
    int center = left + (right - left) >> 1;                                    // 中间元素的索引

    if (A[left] > A[center])                                                    // 使中间元素比左边的元素大
    {
        Swap(&A[left], &A[center]);
    }
    if (A[left] > A[right])                                                     // 使右边的元素比左边的元素大
    {
        Swap(&A[left], &A[right]);
    }
    if (A[center] > A[right])                                                   // 使右边的元素比中间元素大
    {
        Swap(&A[center], &A[right]);
    }

    // 程序执行到这,A[left]<=A[center], A[center]<=A[right]

    return A[right - 1];                                                        // 返回枢轴元素
}
/**
 * @brief 快速排序递归函数
 * 
 * @param A 排序数组
 * @param left 排序区间的左边界
 * @param right 排序区间的右边界
 */
void Quick(ElementType A[], int left, int right)
{
    int l = left, r = right;
    ElementType pivot = Median(A, left, right);                                          // 枢轴元素
    ElementType temp = 0;

    // 循环将比枢轴元素小的元素交换到左边,比枢轴元素大的元素交换到右边
    while (l < r)
    {
        // 循环左指针向右移动,直到找到比枢轴元素大或相等的元素
        while (A[l] < pivot)
        {
            l++;
        }
        // 循环右指针向左移动,直到找到比枢轴元素小或相等的元素
        while (A[r] > pivot)
        {
            r--;
        }

        // 如果l>=r成立,说明privoot的左边的值全部小于等于pivot,右边的值全部大于等于pivot
        if (l >= r)
        {
            break;
        }
  
        // 程序走到这里,说明l所指的元素大于pivot,r所指的元素小于pivot
        Swap(&A[l], &A[r]);

        // 如果交换完后,发现A[l]==pivot,则l++,继续向右移动,否则下次进入循环时,会一直交换元素,卡死
        if (A[l] == pivot)
        {
            l++;
        }
        // 如果交换完后,发现A[r]==pivot,则r--,继续向左移动,否则下次进入循环时,会一直交换元素,卡死
        if (A[r] == pivot)
        {
            r--;
        }
    }

    // 程序执行到这,l>=r
    // 如果l==r成立,必须l++,r--,否则会出现栈溢出
    // 在l==r的情况下不进行任何操作,而是直接跳出循环,
    // 那么下一次递归调用快速排序函数时,相同的left和right参数会再次传入,导致无限递归,最终引发栈溢出
    if (l == r)
    {
        l++;
        r--;
    }

    // 向左递归
    if (left < r)
    {
        Quick(A, left, r);
    }
    // 向右递归
    if (right > l)
    {
        Quick(A, l, right);
    }
}

五、选择排序

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

选择排序

/**
 * @brief 选择排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void SelectionSort(ElementType A[], int N)
{
    int i = 0, j = 0;
    ElementType min = 0;
    for (i = 0; i < N - 1; i++)                                                 // 循环次数为N-1
    {
        min = i;                                                                // 记录最小元素下标
        for (j = i + 1; j < N; j++)
        {
            if (A[j] < A[min])                                                  // 找到最小元素
            {
                min = j;
            }
        }
        Swap(&A[i], &A[min]);
    }
}

六、堆排序

  堆排序(Heap Sort)是一种基于二叉堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组来模拟堆的结构,堆排序可以分为两个主要阶段:建堆和调整堆。这里,我们使用最大堆来实现,逐步将最大堆的第一个元素(最大的元素)与最后一个元素交换,然后数组的长度减 1。

堆排序

/**
 * @brief 堆排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void HeapSort(ElementType A[], int N)
{  
    for (int i = N / 2 - 1; i >= 0; i--)                                         // 构建最大堆
    {
        Heapify(A, N, i);
    }
  
    for (int i = N - 1; i >= 0; i--)                                            // 一个个从堆顶取出元素
    {
        // 将当前最大的元素arr[0]与arr[i]交换
        Swap(&A[0], &A[i]);
  
        Heapify(A, i, 0);                                                       // 重新调整堆结构
    }
}
/**
 * @brief 将数组构建称最大堆
 * 
 * @param A 数组
 * @param N 数组的长度
 * @param i 根结点的索引
 */
void Heapify(int A[], int N, int i) 
{
    int largest = i;                                                            // 初始化largest为根
    int left = 2 * i + 1;                                                       // 左子节点
    int right = 2 * i + 2;                                                      // 右子节点
  
    if (left < N && A[left] > A[largest])                                       // 如果左子节点大于根
    {
        largest = left;
    }

    if (right < N && A[right] > A[largest])                                     // 如果右子节点大于目前已知的最大值
    {
        largest = right;
    }

  
    if (largest != i)                                                           // 如果最大值不是根,则交换
    {
        Swap(&A[i], &A[largest]);

        Heapify(A, N, largest);                                                 // 递归地调整受影响的子树
    }
}

七、归并排序

  归并排序(Merge Sort)是一种分而治之的算法,它将原始数据分成越来越小的部分,直到每个部分只有一个元素,然后将这些部分两两合并成有序的序列,最终合并成完全有序的序列。归并排序是稳定的排序算法,且无论输入数据如何,都能保证 O(nlogn)的时间复杂度。

归并排序

/**
 * @brief 归并排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void MergeSort(ElementType A[], int N)
{
    ElementType * temp = (ElementType *)malloc(N * sizeof(ElementType));
    int width = 0, left = 0, right = 0, mid = 0;

    for (width = 1; width < N; width *= 2)                                              // 计算每一趟归并的宽度
    {
        for (left = 0; left < N - width; left += width * 2)                             // 每次循环完后计算每一趟归并的左边界
        {
            mid = left + width - 1;                                                     // 计算中间索引
            right = ((left + 2 * width - 1) >= N) ? (N - 1) : (left + 2 * width - 1);   // 计算每一趟归并的右边界
            Merge(A, temp, left, mid, right);
        }
    }
    free(temp);
}
/**
 * @brief 合并
 * 
 * @param A 排序数组
 * @param temp 做中转的数组
 * @param left 左边有序序列的初始索引
 * @param mid 中间索引
 * @param right 右边索引
 */
void Merge(ElementType A[], ElementType temp[], int left, int mid, int right)
{
    int i = left;                                                               // 左边有序序列的初始索引
    int j = mid + 1;                                                            // 右边有序序列的初始索引
    int k = left;                                                               // temp数组的初始索引

    // 先把左右两边有序的数据按规则填充到temp数组中,直到有一边处理完毕
    while (i <= mid && j <= right)
    {
        // 左边有序序列的元素小于等于右边有序序列的元素,即将左边的元素填充到temp中
        // 右边有序序列的元素小于左边有序序列的元素,即将右边的元素填充到temp中
        temp[k++] = (A[i] <= A[j]) ? A[i++] : A[j++];
    }

    // 将有剩余数据的一边一次填充到temp中
    // 如果左边有序序列还有剩余的元素,直接填充到temp中
    while (i <= mid)
    {
        temp[k++] = A[i++];
    }
    // 如果右边有序序列还有剩余的元素,直接填充到temp中
    while (j <= right)
    {
        temp[k++] = A[j++];
    }
  
    // 将temp数组中的元素拷贝到A
    // 并不是每次都拷贝所有的元素
    for (i = left; i <= right; i++) {
        A[i] = temp[i];
    }
}

八、基数排序

  基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的基本思想是将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行排序。这样从最低位排序一直到最高位排序完成后,数列就变成一个有序序列。

基数排序

#include <math.h>

/**
 * @brief 基数排序
 * 
 * @param A 排序数组
 * @param N 数组长度
 */
void RadixSort(int A[], int N)
{
    int maxValue = FindMax(A, N);
    int n = DigitCount(maxValue);
    int index = 0; 
    int temp = 0;
  
    PQueue buckets[10];

    for (int i = 0; i < 10; i++)
    {
        buckets[i] = CreateQueue();
    }

    for (int i = 0; i < n; i++)                                                 // 遍历每一位数
    {
        for (int j = 0; j < N; j++)                                             // 遍历数组元素
        {
            temp = A[j] / (int)pow(10, i) % 10;                                 // 获取对应的位
            Enqueue(buckets[temp], A[j]);                                       // 放入到对应的桶中
        }

        index = 0;
        for (int j = 0; j < 10; j++)                                            // 遍历桶
        {
            while (buckets[j]->Front != NULL)                                   // 桶不为空
            {
                A[index++] = Dequeue(buckets[j]);                               // 取出元素放到A数组中
            }
        }
    }
}
/**
 * @brief 获取最大值
 * 
 * @param A 数组
 * @param N 数组长度
 * @return int 最大值
 */
int FindMax(int A[], int N)
{
    int max = A[0];
    for (int i = 1; i < N; i++)
    {
        if (A[i] > max)
        {
            max = A[i];
        }
    }
  
    return max;
}
/**
 * @brief 获取整数的位数
 * 
 * @param n 整数
 * @return int 返回位数
 */
int DigitCount(int n) 
{
    int count = 0;

    if (n == 0)                                                                 // 0被认为有1位
    {
        return 1; 
    }
    n = (n < 0) ? -n : n;                                                       // 如果n是负数,则转为正数
    do 
    {
        n /= 10;                                                                // 除以10
        count++;                                                                // 位数增加
    } while (n != 0);

    return count;
}
posted @   星光映梦  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
· 全程使用 AI 从 0 到 1 写了个小工具
点击右上角即可分享
微信分享提示