数据结构--排序

目录

1. 插入排序

1.1 直接插入排序

1.2 折半插入排序

1.3 希尔排序

2. 交换排序

2.1 冒泡排序

2.2 快速排序

3. 选择排序

3.1 简单选择排序

3.2 堆排序

4. 归并排序

5. 基数排序


1. 插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

  • 动图演示:

  • 代码实现:

1.1 直接插入排序

  • 设置“哨兵”
//插入排序——直接插入排序

typedef int ElemType;
//直接插入排序
void InsertSort(ElemType A[], int n) {
    int i, j;
    for (i = 2; i <= n; i++) { //依次将A[2]~A[n]插入前面已排序序列
        if(A[i] < A[i - 1]) { //若A[i]关键字小于其前驱 将A[i]插入有序表
            A[0] = A[i]; //复制为哨兵 A[0]不存放元素
            for(j = i - 1; A[0] < A[j]; j--) { //从后往前查找待插入位置
                A[j + 1] = A[j];    //向后挪位
            }
            A[j + 1] = A[0]; //复制到插入位置
        }
    }
}

1.2 折半插入排序

  • 设置暂存单元"A[0]"
//插入排序——折半插入排序

typedef int ElemType;
//折半插入排序
void InsertSort(ElemType A[], int n) {
    int i, j, low, high, mid;
    for (i = 2; i <= n; i++) { //依次将A[2]~A[n]插入前面已排序序列
        A[0] = A[i]; //将A[i]暂存到A[0]
        low = 1;
        high = i - 1; //设置折半查找的范围
        while (low <= high) { //折半查找(默认递增有序)
            mid = (low + high) / 2; //取中间点
            if(A[mid] > A[0]) { //查找左半子表
                high = mid - 1;
            } else {
                low = mid + 1;//查找右半子表
            }
        }
        for(j = i - 1; j >= high + 1; --j) {
            A[j + 1] = A[j];    //统一后移元素 空出插入位置
        }
        A[high + 1] = A[0];//插入操作
    }
}

1.3 希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

  • 动图演示:

  • 代码实现:
  • 设置暂存单元"A[0]"
//插入排序——希尔排序

typedef int ElemType;
//希尔排序
void ShellSort(ElemType A[], int n) {
    //当 j <= 0时 插入位置已到
    int dk, i, j;
    for (dk = n / 2; dk >= 1; dk = dk / 2) { //步长变化
        for(i = dk + 1; i <= n; i++)
            if(A[i] < A[i - dk]) { //需将A[i]插入有序增量子表
                A[0] = A[i]; //暂存在A[0]
                for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk) {
                    A[j + dk] = A[j]; //记录后移 查找插入的位置
                }
                A[j + dk] = A[0]; //插入
            }
    }
}

2. 交换排序

2.1 冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

  • 动图演示:

  •  代码实现:
//交换排序——冒泡排序
typedef int ElemType;
//交换函数值
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
//冒泡排序
void BubbleSort(ElemType A[], int n) {
    int i, j;
    bool flag;//本趟排序是否发生交换的标志
    for (i = 0; i < n - 1; i++) {
        flag = false;
        for (j = n - 1; j > i; j--) { //一趟冒泡过程
            if(A[j - 1] > A[j]) { //若为逆序
                swap(A[j - 1], A[j]);//交换
                flag = true;
            }
        }
        if(!flag) { //本趟遍历后没有发生交换 说明表已经有序
            return;
        }
    }
}

2.2 快速排序

  • 算法步骤:
  1. 从数列中挑出一个元素,称为 "基准"(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
  3. 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

  • 动图演示:

  •  代码实现:
//交换排序——快速排序
typedef int ElemType;
int Partition(ElemType A[], int low, int high) { //一趟划分
    ElemType pivot = A[low]; //将当前表中第一个元素设为枢轴 对表进行划分
    while(low < high) { //循环跳出条件
        while (low < high && A[high] >= pivot) {
            --high;
        }
        A[low] = A[high]; //将比枢轴小的元素移动到左端
        while(low < high && A[low] <= pivot) {
            ++low;
        }
        A[high] = A[low]; //将比枢轴大的元素移动到右端
    }
    A[low] = pivot; //枢轴元素存放到最终位置
    return low;
}
void QuickSort(ElemType A[], int low, int high) {
    if(low < high) { //递归跳出的条件
        //Partition()就是划分操作 将表A[low...high]划分为满足上述条件的两个子表
        int pivotpos = Partition(A, low, high);    //划分
        QuickSort(A, low, pivotpos - 1); //一次对两个子表进行递归排序
        QuickSort(A, pivotpos + 1, high);
    }
}

3. 选择排序

3.1 简单选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。

  • 算法步骤

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。重复第二步,直到所有元素均排序完毕。

  • 动图演示:

  •  代码实现:
//选择排序——简单选择排序
typedef int ElemType;
//交换函数值
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
//简单选择排序
void SelectSort(ElemType A[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int min = i;
        for (int j = i + 1; j < n; j++) {
            if(A[j] < A[min]) {
                min = j;
            }
        }
        if(min != i) {
            swap(A[i], A[min]);
        }
    }
}

3.2 堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶(根)堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶(根)堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。

  •  算法步骤
  1. 创建一个堆 H[0……n-1];

  2. 把堆首(最大值)和堆尾互换;

  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

  4. 重复步骤 2,直到堆的尺寸为 1。

动图演示:

  • 代码实现:
  • 设置暂存单元"A[0]"——将A[0]位置空出来 不存放
//选择排序——堆排序
typedef int ElemType;
//交换函数值
void Swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
//建立大根堆
void HeadAdjust(ElemType A[], int k, int len) { //函数 HeadAdjust 将元素 k 为跟的子树进行调整
    A[0] = A[k]; //A[0]暂存子树的根结点
    for(int i = 2 * k; i <= len; i *= 2) { //沿 key 较大的子结点向下筛选
        if(i < len && A[i] < A[i + 1]) { //取 key 较大的子结点的下标
            i++;
        }
        if(A[0] >= A[i]) { //筛选结束
            break;
        } else {
            A[k] = A[i]; //将 A[i]调整到双亲结点上
            k = i; //修改 k 值 以便继续向下筛选
        }
    }
    A[k] = A[0]; //被筛选结点的值放入最终位置
}
void BuildMaxHeap(ElemType A[], int len) {
    for (int i = len / 2; i > 0; i--) { //从 i = ⌈n/2⌉ ~ 1 反复调整堆
        HeadAdjust(A, i, len);
    }
}
//堆排序算法
void HeapSort(ElemType A[], int len) {
    BuildMaxHeap(A, len); //初始建堆
    for(int i = len; i > 0; i--) { //n-1 趟的交换和建堆过程
        Swap(A[i], A[1]); //取出堆顶元素(和堆底元素交换)
        HeadAdjust(A, 1, i - 1); //调整 把剩余的 i-1 个元素整理成堆
    }
}
  • 不设置暂存单元"A[0]"——A[0]位置不空出来
//选择排序——堆排序
typedef int ElemType;
//交换函数值
void Swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
//建立大根堆
void HeadAdjust(ElemType A[], int k, int len) { //函数 HeadAdjust 将元素 k 为跟的子树进行调整
    ElemType temp = A[k]; //temp暂存子树的根结点
    for(int i = 2 * k; i < len; i *= 2) { //沿 key 较大的子结点向下筛选
        if(i < len && A[i] < A[i + 1]) { //取 key 较大的子结点的下标
            i++;
        }
        if(temp >= A[i]) { //筛选结束
            break;
        } else {
            A[k] = A[i]; //将 A[i]调整到双亲结点上
            k = i; //修改 k 值 以便继续向下筛选
        }
    }
    A[k] = temp; //被筛选结点的值放入最终位置
}
void BuildMaxHeap(ElemType A[], int len) {
    for (int i = len / 2; i >= 0; i--) { //从 i = ⌈n/2⌉ ~ 1 反复调整堆
        HeadAdjust(A, i, len);
    }
}
//堆排序算法
void HeapSort(ElemType A[], int len) {
    BuildMaxHeap(A, len); //初始建堆
    for(int i = len - 1; i >= 0; i--) { //n-1 趟的交换和建堆过程
        Swap(A[i], A[0]); //取出堆顶元素(和堆底元素交换)
        HeadAdjust(A, 0, i - 1); //调整 把剩余的 i-1 个元素整理成堆
    }
}

4. 归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  1. 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  2. 自下而上的迭代;
  • 算法步骤:
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

  4. 重复步骤 3 直到某一指针达到序列尾;

  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

  • 动图演示:

  •  代码实现:
//归并排序
typedef int ElemType;
int n = 10;
//构造辅助数组B
ElemType *B = (ElemType *)malloc(n *sizeof(ElemType));
void Merge(ElemType A[], int low, int mid, int high) {
    //表A的两段A[low...mid]和A[mid+1...high]各自有序 将它们合并成一个有序表
    int i, j, k;
    for (k = low; k <= high; k++) {
        B[k] = A[k]; //将A中的所有元素复制到B
    }
    for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) { //比较B中左右两段的元素
        if(B[i] <= B[j]) { //将较小值复制到A
            A[k] = B[i++];
        } else {
            A[k] = B[j++];
        }
    }
//    表未检测完 复制
    while(i <= mid) {
        A[k++] = B[i++];
    }
    while(j <= high) {
        A[k++] = B[j++];
    }
}
void MergeSort(ElemType A[], int low, int high) {
    if(low < high) {
        int mid = (low + high) / 2; //从中划分两个子序列
        MergeSort(A, low, mid); //对左子序列递归排序
        MergeSort(A, mid + 1, high); //对右子序列递归排序
        Merge(A, low, mid, high); //归并
    }
}

5. 基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  • 动图演示:

  •  代码实现:
//基数排序

//辅助函数 求数据的最大位数
int maxbit(int data[], int n) {
    int maxData = data[0];//最大数
    //先求出最大数 再求其位数 这样由原先依次每个数判断其位数 稍微优化点
    for (int i = 1; i < n; i++) {
        if (maxData < data[i]) {
            maxData = data[i];
        }
    }
    int d = 1;//记录最大位数
    int p = 10;
    while (maxData >= p) {
        maxData /= 10;
        ++d;
    }
    return d;
}
//基数排序
void RadixSort(int data[], int n) {
    int d = maxbit(data, n);
    int *tmp = new int[n];
    int *count = new int[10];//计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) { //进行d次排序
        for(j = 0; j < 10; j++) {
            count[j] = 0;//每次分配前清空计数器
        }
        for(j = 0; j < n; j++) {
            k = (data[j] / radix) % 10;//统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++) {
            count[j] = count[j - 1] + count[j];//将tmp中的位置依次分配给每个桶
        }
        for(j = n - 1; j >= 0; j--) { //将所有桶中记录依次收集到tmp中
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) { //将临时数组的内容复制到data中
            data[j] = tmp[j];
        }
        radix = radix * 10;
    }
    delete [] tmp;
    delete [] count;
}
void test() {
    int data[] = {1, 33, 2221, 33, 47, 829, 23, 45, 90, 5, 7000};
    RadixSort(data, 11);
    for (int i = 0; i < 11; i++) {
        cout << data[i] << " ";
    }
    cout << endl;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我这么好看

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值