数据结构与算法·第10章【内部排序】

概念

排序问题可以分为内部排序和外部排序。若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

插入排序

直接插入排序

直接插入排序的基本思想是将待排序的序列分成已排序和未排序的两部分,每次从未排序的部分取出第一个元素,插入到已排序部分合适的位置,直到未排序部分为空为止。具体操作如下:

  1. 初始时,将 R [ 1 ] R[1] R[1] 看作是有序区, R [ 2.. n ] R[2..n] R[2..n] 构成无序区;
  2. 依次将无序区的元素插入到有序区中,使得有序区始终有序。插入操作包括以下三步:
    1)在 R [ 1.. i − 1 ] R[1..i-1] R[1..i1] 中查找 R [ i ] R[i] R[i] 的插入位置,使得 R [ 1.. j ] . k e y ≤ R [ i ] . k e y < R [ j + 1.. i − 1 ] . k e y R[1..j].key ≤ R[i].key < R[j+1..i-1].key R[1..j].keyR[i].key<R[j+1..i1].key
    2)将 R [ j + 1.. i − 1 ] R[j+1..i-1] R[j+1..i1] 中的所有记录均后移一个位置;
    3)将 R [ i ] R[i] R[i] 插入(复制)到 R [ j + 1 ] R[j+1] R[j+1] 的位置上。
  3. 重复执行 2 直到无序区为空,排序完成。
void InsertionSort(SqList& L) {
    // 对顺序表 L 作直接插入排序
    for (int i = 2; i <= L.length; ++i) {
        if (L.r[i].key < L.r[i - 1].key) {
            // 将当前待排序的记录暂存到监视哨中,等待插入
            L.r[0] = L.r[i];
            int j;
            for (j = i - 1; L.r[0].key < L.r[j].key; --j) {
                // 将记录后移,寻找插入位置
                L.r[j + 1] = L.r[j];
            }
            L.r[j + 1] = L.r[0]; // 插入到正确位置
        }
    }
}

最好的情况:

  • 序列顺序有序,比较的次数:n-1,移动的次数:0

最坏的情况:
在这里插入图片描述
时间复杂度大概 O ( n 2 ) O(n^2) O(n2)

其他插入排序

折半插入

void BiInsertionSort(SqList &L) {
    for (int i = 2; i <= L.length; ++i) {
        L.r[0] = L.r[i];      // 将 L.r[i] 暂存到 L.r[0]
        int low = 1, high = i - 1;
        while (low <= high) { 
            int mid = (low + high) / 2; // 折半
            if (L.r[0].key < L.r[mid].key)
                high = mid - 1;   // 插入点在低半区
            else
                low = mid + 1;    // 插入点在高半区
        }
        for (int j = i - 1; j >= high + 1; --j) {
            L.r[j + 1] = L.r[j];      // 记录后移
        }
        L.r[high + 1] = L.r[0];  // 插入
    } 
}

L.r[high + 1] = L.r[0]; // 插入是在 h i g h + 1 high+1 high+1的位置插入(此时,low>high)

希尔排序

希尔排序(又称缩小增量排序)

基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。所谓“宏观”调整,指的是“跳跃式”的插入排序。具体做法为:

1.将记录序列分成若干子序列,分别对每个子序列进行插入排序。

2.待整个序列中的纪录‘基本有序’时,再对全体记录进行一次直接插入排序。

例如:将 n n n个记录分成 d d d个子序列:

R [ 1 ] , R [ 1 + d ] , R [ 1 + 2 d ] , … , R [ 1 + k d ] { R[1],R[1+d],R[1+2d],…,R[1+kd] } R[1]R[1+d]R[1+2d]R[1+kd]

R [ 2 ] , R [ 2 + d ] , R [ 2 + 2 d ] , … , R [ 2 + k d ] { R[2],R[2+d],R[2+2d],…,R[2+kd] } R[2]R[2+d]R[2+2d]R[2+kd]

… …

R [ d ] , R [ 2 d ] , R [ 3 d ] , … , R [ k d ] , R [ ( k + 1 ) d ] { R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] } R[d]R[2d]R[3d]R[kd]R[(k+1)d]

其中, d d d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。

在这里插入图片描述

冒泡排序

// 冒泡排序函数
void bubbleSort(std::vector<int>& arr) {
    int n = arr.size();
    // 外层循环控制遍历次数
    for (int i = 0; i < n - 1; ++i) {
        // 内层循环进行相邻元素的比较和交换
        for (int j = 0; j < n - i - 1; ++j) {
            // 如果当前元素大于下一个元素,交换它们
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
            }
        }
    }
}
  1. 起泡排序的结束条件为,最后一趟没有进行“交换记录”。

  2. 一般情况下,每经过一趟“起泡”,i减1,但并不是每趟都如此。具体来说,每一趟排序时,我们都记录下最后一次交换操作的位置,如果在一趟排序结束之后,最后一次交换操作的位置和上一趟排序结束时的位置相同,那么说明这次排序并没有进行任何交换操作,也就是说从该位置之后的元素已经有序。此时,我们便可以认为序列已经有序了,因此结束算法的执行。

在这里插入图片描述
时间复杂度大概 O ( n 2 ) O(n^2) O(n2)

快速排序

找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。

经过一趟排序之后,记录的无序序列 R [ s . . t ] R[s..t] R[s..t]将分割成两部分: R [ s . . i − 1 ] R[s..i-1] R[s..i1] R [ i + 1.. t ] R[i+1..t] R[i+1..t],且 R [ j ] . k e y ≤ R [ i ] . k e y ≤ R [ p ] . k e y ( s ≤ j ≤ i − 1 )  枢轴  ( i + 1 ≤ p ≤ t ) R[j].key\leq R[i].key \leq R[p].key (s\leq j\leq i-1)~ 枢轴 ~(i+1\leq p\leq t) R[j].keyR[i].keyR[p].key(sji1) 枢轴 (i+1pt)其中 i i i表示枢轴记录的位置, j ≤ i − 1 j \leq i-1 ji1的记录的关键字都小于等于枢轴的关键字, p ≥ i + 1 p \geq i+1 pi+1的记录的关键字都大于等于枢轴的关键字。注意,这里假设枢轴所在的位置不是 s s s t t t,否则就没有对应的一侧了。

在这里插入图片描述

// 函数来执行快速排序
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        // pi 是分区索引,arr[p] 现在是在正确的位置
        int pi = partition(arr, low, high);

        // 分别排序分区前后的数组
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

// 这个函数负责找到分区点并重新排列数组,使得分区点左边的元素都不大于它,右边的元素都不小于它
int partition(int arr[], int low, int high) {
    int pivot = arr[high]; // pivot
    int i = (low - 1); // 比较的索引

    for (int j = low; j <= high - 1; j++) {
        // 如果当前元素小于或等于 pivot
        if (arr[j] <= pivot) {
            i++; // 移动到较高索引处
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i + 1], arr[high]);
    return (i + 1);
}

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

选择排序

在这里插入图片描述
从未排序的序列选择一个最小的元素排到有序序列里

和插入排序的区别:

  • 插入:找到未排序序列的第一个元素,并在有序序列里面查找到插入位置
  • 选择:找到未排序序列最小的元素,并添加到有序序列的最后处
// 函数来执行选择排序
void selectionSort(int arr[], int n) {
    int i, j, min_idx;

    // 一次移动未排序数组的边界
    for (i = 0; i < n-1; i++) {
        // 找到最小元素的索引
        min_idx = i;
        for (j = i+1; j < n; j++)
          if (arr[j] < arr[min_idx])
            min_idx = j;

        // 将找到的最小元素与第 i 个元素交换
        swap(arr[min_idx], arr[i]);
    }
}

在这里插入图片描述

堆排序

在这里插入图片描述
在这里插入图片描述
光看定义还有点不太明白,但是根据就应该很明晰了

建大顶堆

在这里插入图片描述
在这里插入图片描述
自下而上

大顶堆(Max-Heap):

  1. 根节点最大:在大顶堆中,根节点的值是所有节点中最大的。
  2. 子树性质:对于任意节点 i,它的子节点 j(左子节点为 2i+1,右子节点为 2i+2)的值都小于或等于节点 i 的值。
  3. 结构性质:大顶堆是一个完全二叉树,即树中的所有层都是满的,除了可能的最后一层,最后一层的节点从左到右排列。

小顶堆(Min-Heap):

  1. 根节点最小:在小顶堆中,根节点的值是所有节点中最小的。
  2. 子树性质:对于任意节点 i,它的子节点 j(左子节点为 2i+1,右子节点为 2i+2)的值都大于或等于节点 i 的值。
  3. 结构性质:小顶堆也是一个完全二叉树,满足与大顶堆相同的结构性质。

堆的共同性质:

  • 完全二叉树:除了最后一层外,每一层都是满的,并且最后一层的节点从左到右排列。
  • 堆序性质:大顶堆和小顶堆分别满足各自的堆序性质,即父节点的值大于或等于(大顶堆)或小于或等于(小顶堆)其子节点的值。
  • 操作效率:插入和删除操作的时间复杂度通常是 O(log n),其中 n 是堆中元素的数量。这是因为在最坏的情况下,插入或删除操作可能需要节点从叶节点移动到根节点或相反。
  • 数组表示:堆通常使用数组来表示,而不需要使用指针。这是因为堆的完全二叉树性质使得父节点和子节点之间可以通过数组索引来计算。
#include <iostream>
#include <queue>
#include <vector>
#include <functional> // 用于 std::less 和 std::greater

int main() {
    // 大顶堆
    std::priority_queue<int> max_heap;

    // 小顶堆
    std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;

    // 向大顶堆添加元素
    max_heap.push(30);
    max_heap.push(10);
    max_heap.push(20);
    max_heap.push(5);

    // 向小顶堆添加元素
    min_heap.push(30);
    min_heap.push(10);
    min_heap.push(20);
    min_heap.push(5);

    // 打印大顶堆的元素
    std::cout << "Max Heap: ";
    while (!max_heap.empty()) {
        std::cout << max_heap.top() << " ";
        max_heap.pop();
    }
    std::cout << std::endl;

    // 打印小顶堆的元素
    std::cout << "Min Heap: ";
    while (!min_heap.empty()) {
        std::cout << min_heap.top() << " ";
        min_heap.pop();
    }
    std::cout << std::endl;

    return 0;
}

归并排序

  • 归并排序的过程基于下列基本思想进行: 将两个或两个以上的有序子序列 “归并” 为一个有序序列
  • 在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列

在这里插入图片描述
比较简单,看一下即可

习题

各个排序

在这里插入图片描述
最后1个考试不涉及

  • 直接插入排序:503 087 512 061 908 170 897 275 653 426
    第一趟结果:087 503 512 061 908 170 897 275 653 426
    第二趟结果:087 503 512 061 908 170 897 275 653 426
    第三趟结果:061 087 503 512 908 170 897 275 653 426
    第四趟结果:061 087 503 512 908 170 897 275 653 426
    第五趟结果:061 087 170 503 512 908 897 275 653 426
    第六趟结果:061 087 170 503 512 897 908 275 653 426
    第七趟结果:061 087 170 275 503 512 897 908 653 426
    第八趟结果:061 087 170 275 503 512 653 897 908 426
    第九趟结果:061 087 170 275 426 503 512 653 897 908
    粗体为已经排好序的序列

  • 希尔排序初始关键字: 503 087 512 061 908 170 897 275 653 426
    第一趟结果:d[1]=5 170 087 275 061 426 503 897 512 653 908
    第二趟结果:d[2]=3 061 087 275 170 426 503 897 512 653 908
    第三趟结果:d[3]=1 061 087 170 275 426 503 512 653 897 908
    主要注意一下希尔排序是以下标号的后x个作排序——在d[1]=5,a[0]=503是a[5]=170排序的

  • 快速排序在这里插入图片描述
    注意,快速排序不是把比Key小的数直接随便放到Key前面,是用low和high遍历出来的

  • 堆排序
    在这里插入图片描述
    小顶堆

  • 归并排序(自底向上)
    503 087 512 061 908 170 897 275 653 426
    (087 503) (061 512) (170 908) (275 897) (426 653)
    (061 087 503 512) (170 275 897 908) (426 653)
    (061 087 170 275 503 512 897 908) (426 653)
    (061 087 170 275 426 503 512 653 897 908)

堆排序

在这里插入图片描述
在这里插入图片描述
第4问的答案应该是错的

监视哨

在这里插入图片描述

void directInsertSort(int L[], int k) {
    int i, j;
    for (i = 2; i <= k; i++) {
        L[k+1] = L[i]; 
        j = i - 1;
        while (L[j] > L[0]) {
            L[j + 1] = L[j];
            j--;
        }
        L[j + 1] = L[k+1]; 
    }
}

设计算法

在这里插入图片描述

void process(int A[n]) {
    int low = 0;
    int high = n - 1;
    while (low < high) {
        while (low < high && A[low] < 0)
            low++;
        while (low < high && A[high] > 0)
            high++;
        if (low < high) {
            // 交换 A[low] 和 A[high]
            int temp = A[low];
            A[low] = A[high];
            A[high] = temp;
            low++;
            high--;
        }
    }
    return;
}

双指针法
时间复杂度 O ( n ) O(n) O(n)

荷兰国旗问题

在这里插入图片描述

typedef enum {RED, WHITE, BLUE} color; // 定义枚举类型表示三种颜色
void Flag_Arrange(color a[], int n) {
    int i = 0;
    int j = 0;
    int k = n - 1;
    while (j <= k) {
        switch (a[j]) {
            case RED:
                // a[i] 与 a[j] 交换
                // 增加 i 和 j 的值,同时继续处理下一个元素
                swap(a[i], a[j]);
                i++;
                j++;
                break;
            case WHITE:
                // 当遇到白色时,只需要将 j 向右移动一位
                j++;
                break;
            case BLUE:
                // a[j] 与 a[k] 交换
                // 不增加 j 的值,因为可能需要再次检查交换后的 a[j]
                // 减少 k 的值,将蓝色元素移至数组末尾
                swap(a[j], a[k]);
                k--;
                break;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值