当前位置: 首页 > news >正文

数据结构第6篇:手撕排序算法(插入、希尔、堆)

1. 排序的概念及其运用

1.1 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能再内外存之间移动数据的排序。

1.2 排序运用

排序在现实中可以作为一个筛选的作用

1.3 常见的排序算法

 2. 排序算法的实现

//排序的实现接口
//直接插入排序
void TestInsertsort(int* arr, int n);

//希尔排序
void Shellsort(int* arr, int n);

//选择排序
void selectsort(int* arr, int n);

//堆排序
void Adjustdwon(int* arr, int n, int root);
void HeanSort(int* arr, int n);

//冒泡排序
void BubbleSort(int* arr, int n);

//快速排序
void QuickSort(int* arr, int n);

//归并排序
void MergeSort(int* arr, int n);

3. 插入排序

3.1 直接插入排序

3.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

实际中我们玩扑克牌时,就用了插入排序的思想

3.1.2  直接插入排序:

当插入第i(i>=1)个元素时,前面的array[0],array[1],.....,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],...的排序码顺序进行比较,找到插入位置即将array[i]插入原来位置上的元素顺序后移

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定
3.1.3  直接插入排序的代码实现
//一、插入排序
//1.直接插入排序
void TestInsertsort(int* arr, int n)
{
    int i = 0;
    for (i = 0; i < n - 1; i++){
        int end = i;
        int tmap = arr[end + 1];
        while (end >= 0) {
            if (arr[end] > tmap)
            {
                arr[end + 1] = arr[end];
                end--;
            }
            else {
                break;
            }
        }
        arr[end + 1] = tmap;
    }
}

 3.2 希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成个组,所有距离为gap的记录并分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

对组间隔为gap的预排序,gap由大变小

gap越大,大的数可以越快到后面,小的数可以越快到前面

gap越大,预排完越不接近有序

gap越小,越接近有序

当gap==1时就是直接插入排序

注:希尔排序只是在直接插入排序上多了一道步骤,也可以把希尔排序理解成插入排序的优化。

希尔排序分两步:

1、预排序(优化序列):先通过gap分组将数组进行预排序,尽量将数组优化的有序。

2、排序:当gap==1时,就是正常的直接插入排序,前面将数组优化就是为了直接插入排序是效率更高。

希尔排序的代码实现:

//2. 希尔排序
void ShellSort(int* arr, int n) {

    int gap = n;
    while (gap > 1) {
        gap /= 2;
        //或者
        //gap /= 3 + 1;都可以
        //只不过一定要让gap最后等于1,因为gap大于1都是预排序
        //只有gap等于1时才是直接插入排序
        int i = 0;
        //把间隔为gap的多组数据同时排
        for (i = 0; i < n - gap; i++)
        {
            int end = i;
            int tmap = arr[end + gap];
            while (end >= 0) {
                if (arr[end] > tmap) {
                    arr[end + gap] = arr[end];
                    end -= gap;
                }
                else {
                    break;
                }
            }
            arr[end + gap] = tmap;
        }
    }
}

4. 测试排序的性能对比

有人可能会疑问,希尔排序需要预排序这么多次,那会比直接插入排序还要效率吗?如果想知道我们就可以把这两种排序的运行时间拿出来比较

void TestOp()
{
    srand(time(0));
    //开辟6个大小为100000的空间作为待排序数组
    const int n = 100000;
    int* a1 = (int*)malloc(n * sizeof(int));
    int* a2 = (int*)malloc(n * sizeof(int));
    int* a3 = (int*)malloc(n * sizeof(int));
    int* a4 = (int*)malloc(n * sizeof(int));
    int* a5 = (int*)malloc(n * sizeof(int));
    int* a6 = (int*)malloc(n * sizeof(int));
    int i = 0;
    //给6个数组赋一样值
    for (i = 0; i < n; i++) {
        a1[i] = rand();
        a2[i] = a1[i];
        a3[i] = a1[i];
        a4[i] = a1[i];
        a5[i] = a1[i];
        a6[i] = a1[i];
    }
    //计算进入排序之前和排序之后的时间
    int begin1 = clock();
    TestInsertsort(a1, n);
    int end1 = clock();

    int begin2 = clock();
    ShellSort(a2, n);
    int end2 = clock();

    int begin3 = clock();
    selectsort(a3, n);
    int end3 = clock();

    int begin4 = clock();
    BubbleSort(a4, n);
    int end4 = clock();

    int begin5 = clock();
    QuickSort(a5, n);
    int end5 = clock();

    int begin6 = clock();
    MergeSort(a6, n);
    int end6 = clock();

   printf("%d\n", end1 - begin1);
   printf("%d\n", end2 - begin2);
   printf("%d\n", end3 - begin3);
   printf("%d\n", end4 - begin4);
   printf("%d\n", end5 - begin5);
   printf("%d\n", end6 - begin6);
}

测试结果:

注:以上单位是毫秒

看似希尔步骤比较麻烦,实际上希尔只是先给数组预排序进行优化,最后再整体对优化后的序列进行排序,所以要比直接插入排序效率很多。

5. 选择排序和堆排序

5.1 基本思想

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

5.1.1 直接选择排序:
  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

单向选择排序的实现:

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
void SelectSort(int* arr, int sz) {
    int a = 0;//总是表示未排序的第一个位置,排序一次转下一个元素位置
    while (a < sz - 1) {
        int j = a;//记录最小值的下标
        int i = 0;//遍历a后面的序列找最小值
        for (i = a + 1; i < sz; i++) {
            if (arr[i]<arr[j]) { 
                j = i;//找到了就记录这个最小值的下标
            }
        }
        //调换位置,将未排序序列的最小值换到第一个位置
        Swap(&arr[a], &arr[j]);
        //转移未排序序列的第一个位置
        a++;
    }
}
int main() {
    int arr[] = { 22,66,43,25,3,10,2,75,21,62 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    SelectSort(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

双向选择排序实现:

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
void SelectSort(int* arr, int sz) {
    int begin = 0, end = sz - 1;
    while (begin < end) {
        int mini = begin, maxi = begin;
        int i = 0;
        for (i = begin; i <= end; i++) {
            if (arr[i] > arr[maxi]) {
                maxi = i;
            }
            if (arr[i] < arr[mini]) {
                mini = i;
            }
        }
        Swap(&arr[begin], &arr[mini]);
        if (maxi == begin) {
            maxi = mini;
        }
        Swap(&arr[end], &arr[maxi]);
        begin++;
        end--;
    }
}
int main()
{
    int arr[] = { 10,6,3,9,4,2,7,8,5,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    SelectSort(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

5.2 堆排序

什么是堆?堆其实是一种二叉树的存储形式,就是二叉树以数组的形式进行存储。

堆排序:其实就是将数组想象成完全二叉树(堆)再进行排序,物理结构上是数组,但是逻辑结构一定要是二叉树。

如下图:

堆分为两种:

大堆要求:树中的所有父亲都大于等于孩子

小堆要求:树中的所有父亲都小于等于孩子

5.2.1 自底向上的建堆方式

该建堆方式是从倒数第二层的节点(叶子节点的上一层)开始,从右往左,从下到上的向下进行调整。

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

堆排序分为两个步骤:

  1. 建堆:就是将数组建立成堆,相当于序列优化。
  2. 排序

注:排升序建大堆,排降序建小堆,这样的效率最高

堆排序的实现:

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
void AdjustDown(int* arr, int n, int i) {
    //从上往下建立堆
    int parent = i;//当前根节点
    int child = parent * 2 + 1;//左孩子节点
    while (child < n) {
        //判断右孩子是否比左孩子大,是的话child就改为右孩子节点下标,建立小堆与之相反
        if (child + 1 < n && arr[child] < arr[child + 1]) {
            child += 1;
        }
        //判断父节点是否比子节点小,如果是的话就和子节点进行交换,建立小堆与之相反
        if (arr[parent] < arr[child]) {
            Swap(&arr[parent], &arr[child]);
            //交换完需要对指向父节点和子节点的下标进行调整
            parent = child;
            child = parent * 2 + 1;
        }
        else {
            break;
        }
    }
}
void HeapSort(int* arr, int n) {
    //堆排序步骤:
    //1.建堆
    int i = 0;
    //从非叶子节点的最后节点开始建堆
    for (i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(arr, n, i);
    }
    //2.排序
    int end = n - 1;//先选中序列最后一个位置
    while (end > 0) {
        Swap(&arr[0], &arr[end]);//已经建立过大堆,所以第一个根节点必定是最大的数,所以与最后一个位置交换
        AdjustDown(arr, end, 0);//继续选出堆的最大值在第一个节点
        end--;
    }
}
int main() {
    int arr[] = { 22,66,43,25,3,10,2,75,21,62 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    HeapSort(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

堆排序画图解析过程:

第一步:建堆

第二步:排序

整体步骤展示图:

6. 冒泡排序

冒泡排序是一种数组排序的算法,这种排序就像汽水里的气泡一样不停的从下往上面冒泡,所以名为冒泡排序(bubble sort)。

冒泡排序的算法思想就是需要排序n-1趟,每一趟排出最大的数在位置最后。因为这种方法最多n-1趟就可以将数组排序完毕。因为每次筛选最大值排在最后,有n个数,n-1个数筛选完后最后一个数必定是在第一个,也就是最小值。经过第一趟排序需要n-1次判断两个相邻的数,如果前面大于后面的就调换。算上排最大值本身,与其他的值经过筛选判断也只需要n-1次判断排出最大值。每一趟排出最大数下一趟排序的n-1需要再减去前面已经排过的趟数。因为每一趟都排出最大值下一趟就不需要对最大数也进行判断,只需要判断已排序最大值前面的那些值就可以了。

既然知道了冒泡排序算法的思想,那接下来就实现冒泡排序算法:

int main()
{
    int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i, j;
    for (i = 0; i < sz - 1; i++)//循环排序n-1趟
    {
        int flag = 1;//假设顺序是正确的
        for (j = 0; j < sz - 1 - i; j++)//循环n-1-i次判断并调换找出最大值
        {
            if (arr[j] > arr[j + 1])
            {
                int s = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = s;
                flag = 0;//设置为需要排序
            }
        }
        if (flag == 1)//假设一趟下来没有任何排序的值,说明已经不再需要排序,跳出循环排序
        {
            break;
        }
    }
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

运行结果:

7. 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

将区间按照基准值划分为左右两半部分的常见方式有:

整体实现思想:

 快速排序先拿出一个关键字,然后把其余数组成的序列排序为两个,分别是小于这个关键字key的左子序列还有大于这个关键字的右子序列,然后通过递归将左子序列和右子序列分别按照以上的方法排序,直到序列个数递归为1个结束,是类似于二叉树的排序。

快速排序特性总结:

  1. 快速排序的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N * logN)。

快速排序代码实现:

1.挖坑法

void QuickSort(int* arr, int left, int right) {
    //递归返回判断条件,如果left大于或等于right就说明只有一个数或没有数可以排序,就返回
    if (left >= right)
        return;
    int begin = left, end = right;//定义一个开始位置和一个末尾位置
    int pivot = begin;//定义一个支点
    int key = arr[pivot];//拿走支点上的数值,腾出位置
    while (begin < end) {
        //找右子序列有没有小于key的数
        while (begin < end && arr[end] >= key) {
            end--;
        }
        //有就交换一下,然后end成为新支点pivot用来接收下一个大于key的数
        arr[pivot] = arr[end];
        pivot = end;
        //找左子序列有没有大于key的数
        while (begin < end && arr[begin] <= key) {
            begin++;
        }
        //有就交换一下,然后begin成为新支点pivot用来接收下一个小于key的数
        arr[pivot] = arr[begin];
        pivot = begin;
    }
    //直到begin==end就可以将关键词key放入此位置,此时begin==end==pivot
    arr[begin] = key;
    //左子序列排序
    QuickSort(arr, left, pivot - 1);
    //右子序列排序
    QuickSort(arr, pivot + 1, right);
}
int main()
{
    int arr[] = { 6,9,3,10,4,2,7,8,5,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSort(arr, 0, sz - 1);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

快速排序的运算速率很好,但是我们要考虑到快速排序的最坏情况,那快速排序的最坏情况是什么?答案如下:

1)数组已经是正序(same order)排过序的。
2)数组已经是倒序排过序的。
3)所有的元素都相同(1、2的特殊情况)

为了避免快速排序退化为最坏情况,可以采用以下几种策略来选择枢轴:

  1. 随机选择枢轴:通过随机选择枢轴,可以避免每次都选择到最差的枢轴位置,从而提高算法的平均性能2。

  2. 三数取中:选择第一个、中间和最后一个元素的中值作为枢轴,可以有效避免极端情况的出现3。

  3. 改进的分区方法:例如三路快速排序,将数组分为小于、等于和大于枢轴的三部分,可以提高处理重复元素的效率3。

我们就用三数取中的方法来优化快速排序,三数取中就是选中三个数取中间值。可以有效防止排到有序数组。

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
int GetMidIndex(int* arr,int left,int right){
    //取两个序列的中间下标,两数相加向的和右移动一位就相当于除以2
    int mid = (left + right) >> 1;
    //返回arr[left]/arr[right]/arr[mid]三个数之间的中间值
    if (arr[left]<arr[mid]) {
        if (arr[mid] < arr[right]) {
            return mid;
        }
        else if (arr[left] > arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
    else {//arr[left]>arr[mid]
        if (arr[mid] > arr[right]) {
            return mid;
        }
        else if (arr[left] < arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
}
void QuickSort(int* arr, int left, int right) {
    //递归返回判断条件,如果left大于或等于right就说明只有一个数或没有数可以排序,就返回
    if (left >= right)
        return;
    //三数取中操作
    //取中位数
    int index = GetMidIndex(arr, left, right);
    //将序列的第一个数和得到的中位数交换
    Swap(&arr[left], &arr[index]);
    /
    int begin = left, end = right;//定义一个开始位置和一个末尾位置
    int pivot = begin;//定义一个支点
    int key = arr[pivot];//拿走支点上的数值,腾出位置
    while (begin < end) {
        //找右子序列有没有小于key的数
        while (begin < end && arr[end] >= key) {
            end--;
        }
        //有就交换一下,然后end成为新支点pivot用来接收下一个大于key的数
        arr[pivot] = arr[end];
        pivot = end;
        //找左子序列有没有大于key的数
        while (begin < end && arr[begin] <= key) {
            begin++;
        }
        //有就交换一下,然后begin成为新支点pivot用来接收下一个小于key的数
        arr[pivot] = arr[begin];
        pivot = begin;
    }
    //直到begin==end就可以将关键词key放入此位置,此时begin==end==pivot
    arr[begin] = key;
    //左子序列排序
    QuickSort(arr, left, pivot - 1);
    //右子序列排序
    QuickSort(arr, pivot + 1, right);
}

三数取中是为了让快速排序遇到最坏的打算有解决方式,但是还有没有可以优化的地方呢?答案是有的,看下图

假设我有一个500w数值的序列,我使用快速排序来排这个序列,刚开始还好,但是当最后会被分为许多小于10的序列,就是红线划分的最后几层。这是候光函数递归就50w次,我们可不可以想个办法来将这最后的几层调用全部消除呢?

我们可以使用判断小区间的方法来判断,如果达到这个规定区间大小判定,我们就不再递归下去,因为递归下去还要继续递归,我们到了这个区间就直接用其他排序帮忙排序一下,这样就不会再往下递归几十万次了。那我们应该用什么排序?答案是:快速插入排序,因为快速插入排序面对较小的序列会非常的效率。

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
void TestInsertSort(int* arr, int n) {
    int i = 0;
    for (i = 0; i < n - 1; i++) {
        int end = i;
        int tmap = arr[i + 1];
        while (end >= 0) {
            if (arr[end] > tmap) {
                arr[end + 1] = arr[end];
                end--;
            }
            else {
                break;
            }
        }
        arr[end + 1] = tmap;
    }
}
int GetMidIndex(int* arr, int left, int right) {
    //取两个序列的中间下标,两数相加向的和右移动一位就相当于除以2
    int mid = (left + right) >> 1;
    //返回arr[left]/arr[right]/arr[mid]三个数之间的中间值
    if (arr[left] < arr[mid]) {
        if (arr[mid] < arr[right]) {
            return mid;
        }
        else if (arr[left] > arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
    else {//arr[left]>arr[mid]
        if (arr[mid] > arr[right]) {
            return mid;
        }
        else if (arr[left] < arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
}
void QuickSort(int* arr, int left, int right) {
    //递归返回判断条件,如果left大于或等于right就说明只有一个数或没有数可以排序,就返回
    if (left >= right)
        return;
    //三数取中操作
    //取中位数
    int index = GetMidIndex(arr, left, right);
    //将序列的第一个数和得到的中位数交换
    Swap(&arr[left], &arr[index]);
    /
    int begin = left, end = right;//定义一个开始位置和一个末尾位置
    int pivot = begin;//定义一个支点
    int key = arr[pivot];//拿走支点上的数值,腾出位置
    while (begin < end) {
        //找右子序列有没有小于key的数
        while (begin < end && arr[end] >= key) {
            end--;
        }
        //有就交换一下,然后end成为新支点pivot用来接收下一个大于key的数
        arr[pivot] = arr[end];
        pivot = end;
        //找左子序列有没有大于key的数
        while (begin < end && arr[begin] <= key) {
            begin++;
        }
        //有就交换一下,然后begin成为新支点pivot用来接收下一个小于key的数
        arr[pivot] = arr[begin];
        pivot = begin;
    }
    //直到begin==end就可以将关键词key放入此位置,此时begin==end==pivot
    arr[begin] = key;
    //我们不直接函数递归了,我们在前面加个判断
    //左子序列排序
    //QuickSort(arr, left, pivot - 1);
    //右子序列排序
    //QuickSort(arr, pivot + 1, right);
    if (pivot - 1 - left > 10) {
        QuickSort(arr, left, pivot - 1);
    }
    else {
        TestInsertSort(arr + left, pivot - 1 - left + 1);
    }
    if (right - (pivot + 1) > 10) {
        QuickSort(arr, pivot + 1, right);
    }
    else {
        TestInsertSort(arr + pivot + 1, right - (pivot + 1) + 1);
    }
}

注:如果一开始要排序的就是较小的序列那就相当于插入排序了,建议当序列特别大的时候用这个优化,小序列继续用快速排序。

2. 左右指针法

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
int ParkSort2(int* arr, int left, int right)
{
    //三数取中操作
    //取中位数
    int index = GetMidIndex(arr, left, right);
    //将序列的第一个数和得到的中位数交换
    Swap(&arr[left], &arr[index]);
    /
    int begin = left, end = right;//定义一个开始位置和一个末尾位置
    int keyi = begin;//拿走支点上的数值,腾出位置
    while (begin < end) {
        //找右子序列有没有小于key的数
        while (begin < end && arr[end] >= arr[keyi]) {
            end--;
        }
        //找左子序列有没有大于key的数
        while (begin < end && arr[begin] <= arr[keyi]) {
            begin++;
        }
        Swap(&arr[begin], &arr[end]);
    }
    Swap(&arr[begin], &arr[keyi]);

    return begin;//返回支点所在位置
}
void QuickSort(int* arr, int left, int right) {
    //递归返回判断条件,如果left大于或等于right就说明只有一个数或没有数可以排序,就返回
    if (left >= right)
        return;
    int KeyIndx = ParkSort2(arr, left, right);
    //我们不直接函数递归了,我们在前面加个判断
    //左子序列排序
    //QuickSort(arr, left, pivot - 1);
    //右子序列排序
    //QuickSort(arr, pivot + 1, right);
    if (KeyIndx - 1 - left > 10) {
        QuickSort(arr, left, KeyIndx - 1);
    }
    else {
        TestInsertSort(arr + left, KeyIndx - 1 - left + 1);
    }
    if (right - (KeyIndx + 1) > 10) {
        QuickSort(arr, KeyIndx + 1, right);
    }
    else {
        TestInsertSort(arr + KeyIndx + 1, right - (KeyIndx + 1) + 1);
    }
}

左右指针法和挖坑法类似,不同的是左右指针法是两边指针都动,分别找到比key大的数和比key小的数再进行交换。而挖坑法是一个指针作为那个坑,另一边的指针找到比key小的数就丢到这个坑里,另一边指针就成为新的坑,然后原来的坑变为可移动指针找比key大的数,丢到现在的坑,然后这个位置又变为坑。

简单理解:

挖坑法:左边指针固定成为坑,右边指针找指定值,找到了右边这个位置就固定为坑,左边位置找指定值

左右指针法:两边同时找最大和最小的值,都找到了进行交换。 

3.前后指针法

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
int ParkSort3(int* arr, int left, int right)
{
    int prev = left, cur = left + 1;//定义一个前后指针
    int keyi = left;
    while (cur <= right)
    {
        if (arr[cur] < arr[keyi]) {
            ++prev;
            Swap(&arr[prev], &arr[cur]);
        }
        cur++;
    }
    Swap(&arr[prev], &arr[keyi]);
    return prev;
}
void QuickSort(int* arr, int left, int right) {
    //递归返回判断条件,如果left大于或等于right就说明只有一个数或没有数可以排序,就返回
    if (left >= right)
        return;
    int KeyIndx = ParkSort3(arr, left, right);
    //我们不直接函数递归了,我们在前面加个判断
    //左子序列排序
    //QuickSort(arr, left, pivot - 1);
    //右子序列排序
    //QuickSort(arr, pivot + 1, right);
    if (KeyIndx - 1 - left > 10) {
        QuickSort(arr, left, KeyIndx - 1);
    }
    else {
        TestInsertSort(arr + left, KeyIndx - 1 - left + 1);
    }
    if (right - (KeyIndx + 1) > 10) {
        QuickSort(arr, KeyIndx + 1, right);
    }
    else {
        TestInsertSort(arr + KeyIndx + 1, right - (KeyIndx + 1) + 1);
    }
}

前后指针法的思想就是定义两个指针,一个在前面,一个在后面。前面的指针去探路,找到比key小的数就和后面的指针交换,交换时后面的指针才往前移动,最后前面指针到尾,就将后面指针位置的值和key交换,最后就是key左边都是比key小的数,key右边都是比key大的数。

建议快速排序时使用挖坑法,左右指针和前后指针两个快速排序法知道一下就行,因为有些题考的是快速排序的第一次排序序列是什么,因为这三个排序法第一次排序后的序列可能并不相同,虽然一开始需要排序的序列相同。

8. 归并排序

基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即使每个序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序核心步骤:

归并排序的递归实现:

void _MgregSort(int* arr, int left, int right, int* tmp)
{
    if (left >= right)
        return;
    int mid = (left + right) >> 1;//拿到两个数的和除以2的结果
    //递归排完序再回来对整体的序列进行排序
    _MgregSort(arr, left, mid, tmp);
    _MgregSort(arr, mid + 1, right, tmp);
    //定义两个区间,然后开始将递归排序过的arr归并排序到临时数组tmp
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    //index要赋值为left是因为后面需要和arr对齐,将tmp中的元素赋值给arr
    int index = left;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (arr[begin1] < arr[begin2]) {
            tmp[index++] = arr[begin1++];
        }
        else {
            tmp[index++] = arr[begin2++];
        }
    }
    //看看哪个区间有剩余的值还没有归并到临时数组tmp
    while (begin1 <= end1)
    {
        tmp[index++] = arr[begin1++];
    }
    while (begin2 <= end2)
    {
        tmp[index++] = arr[begin2++];
    }
    //将临时数组里排好序的序列赋值给原数组arr
    int i = 0;
    for (i = left; i <= right; i++) {
        arr[i] = tmp[i];
    }
}
void MgregSort(int* arr, int n) {
    //建立一个临时数组
    int* tmp = (int*)malloc(n * sizeof(int));
    _MgregSort(arr, 0, n - 1, tmp);
    free(tmp);
}
int main()
{
    int arr[] = { 6,9,3,10,4,2,7,8,5,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    MgregSort(arr,sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

9. 排序算法复杂度及稳定性分析

10. 使用非递归实现快速排序和归并排序

我们如何使用非递归来实现快速排序呢?答案是:用栈实现,用栈模拟递归,递归每一次需要分割序列的头和尾,我们就将这些分割序列的头和尾压到栈里面,然后每次取出两个代表序列头和尾进行一次排序。

非递归快速排序代码实现:

void Swap(int* a, int* b) {
    int s = *a;
    *a = *b;
    *b = s;
}
void TestInsertSort(int* arr, int n) {
    int i = 0;
    for (i = 0; i < n - 1; i++) {
        int end = i;
        int tmap = arr[i + 1];
        while (end >= 0) {
            if (arr[end] > tmap) {
                arr[end + 1] = arr[end];
                end--;
            }
            else {
                break;
            }
        }
        arr[end + 1] = tmap;
    }
}
int GetMidIndex(int* arr, int left, int right) {
    //取两个序列的中间下标,两数相加向的和右移动一位就相当于除以2
    int mid = (left + right) >> 1;
    //返回arr[left]/arr[right]/arr[mid]三个数之间的中间值
    if (arr[left] < arr[mid]) {
        if (arr[mid] < arr[right]) {
            return mid;
        }
        else if (arr[left] > arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
    else {//arr[left]>arr[mid]
        if (arr[mid] > arr[right]) {
            return mid;
        }
        else if (arr[left] < arr[right]) {
            return left;
        }
        else {
            return right;
        }
    }
}
int ParkSort1(int* arr, int left, int right)
{
    //三数取中操作
    //取中位数
    int index = GetMidIndex(arr, left, right);
    //将序列的第一个数和得到的中位数交换
    Swap(&arr[left], &arr[index]);
    /
    int begin = left, end = right;//定义一个开始位置和一个末尾位置
    int pivot = begin;//定义一个支点
    int key = arr[pivot];//拿走支点上的数值,腾出位置
    while (begin < end) {
        //找右子序列有没有小于key的数
        while (begin < end && arr[end] >= key) {
            end--;
        }
        //有就交换一下,然后end成为新支点pivot用来接收下一个大于key的数
        arr[pivot] = arr[end];
        pivot = end;
        //找左子序列有没有大于key的数
        while (begin < end && arr[begin] <= key) {
            begin++;
        }
        //有就交换一下,然后begin成为新支点pivot用来接收下一个小于key的数
        arr[pivot] = arr[begin];
        pivot = begin;
    }
    //直到begin==end就可以将关键词key放入此位置,此时begin==end==pivot
    arr[begin] = key;
    return begin;//返回支点所在位置
}
//使用栈排序//
void QuickSortNonR(int* arr, int n) 
{
    //创建一个栈
    ST st;
    StackInit(&st);
    //先把一个完整序列的头和尾压进去
    StackPush(&st, n - 1);
    StackPush(&st, 0);
    //以栈是否为空判断开始遍历排序
    while (StackEmpty(&st))
    {
        //从栈中获取序列的头和尾
        int left = StackTop(&st);
        StackPop(&st);
        int right = StackTop(&st);
        StackPop(&st);
        //将获取好后序列头和尾的left和right传参,开始第一次排序
        int KeyIndx = ParkSort1(arr, left, right);
        //第一次排序结束将分别将左序列的头尾和右序列的头尾分别压入栈中
        if (KeyIndx + 1 < right) {
            StackPush(&st, right);
            StackPush(&st, KeyIndx + 1);
        }
        if (left < KeyIndx - 1) {
            StackPush(&st, KeyIndx - 1);
            StackPush(&st, left);
        }
        //下一次循环将继续取出两个值(头和尾)
        //按照上面的步骤遍历,直到循环结束排序完成,栈调用和递归调用的顺序相同
    }
    StackDestory(&st);
}
int main()
{
    int arr[] = { 6,9,3,10,4,2,7,8,5,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSortNonr(arr,sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

非递归归并排序代码实现:

void MergeSortNonr(int* arr, int n)
{
    int* tmp = (int*)malloc(n * sizeof(int));
    //gap来控制两个数量为gap的序列来合并
    int gap = 1;
    while (gap < n) {
        int i = 0;
        for (i = 0; i < n; i += 2 * gap) {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + gap * 2 - 1;
            //防止右子序列超出范围
            //因为序列是按整数倍去归的
            //所以如果序列不是整数倍就会导致左序列有了但没有右序列,或者右序列不能对齐左序列
            //我们就需要判断调整
            if (begin2 >= n)
                break;
            if (end2 >= n) {
                end2 = n - 1;
            }
            int index = i;
            while (begin1 <= end1 && begin2 <= end2) {
                if (arr[begin1] < arr[begin2]) {
                    tmp[index++] = arr[begin1++];
                }
                if (arr[begin2] < arr[begin1]) {
                    tmp[index++] = arr[begin2++];
                }
            }
            while (begin1 <= end1) {
                tmp[index++] = arr[begin1++];
            }
            while (begin2 <= end2) {
                tmp[index++] = arr[begin2++];
            }
            int j = 0;
            for (j = 0; j <= end2; j++) {
                arr[j] = tmp[j];
            }
        }
        //每次排完gap*2
        gap *= 2;
    }
    //以上的非递归就是模仿递归从递归的尽头开始往回排的
}
int main()
{
    int arr[] = { 6,9,3,10,4,2,7,8,5,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    MergeSortNonr(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

归并排序也可以叫做外排序,前面的那些排序叫做内排序,外排序是对需要存储硬件的数据进行排序。

http://www.dtcms.com/a/106421.html

相关文章:

  • 【通用级联选择器回显与提交处理工具设计与实现】
  • 中和农信:让金融“活水”精准浇灌乡村沃土
  • RustDesk 开源远程桌面软件 (支持多端) + 中继服务器伺服器搭建 ( docker版本 ) 安装教程
  • windows使用Python调用7-Zip【按大小分组】压缩文件夹中所有文件
  • C# Winform 入门(3)之尺寸同比例缩放
  • 山东大学《多核平台下的并行计算》实验笔记
  • Mysql+Demo 获取当前日期时间的方式
  • 17查询文档的方式
  • CASAIM与哈尔滨电气集团达成战略合作,三维智能检测技术赋能电机零部件生产智造升级
  • 【DRAM存储器四十九】LPDDR5介绍--LPDDR5的低功耗技术之power down、deep sleep mode
  • ContextVars 在 FastAPI 中的使用
  • 最新26考研资料分享考研资料合集 百度网盘(仅供参考学习)
  • 逻辑漏洞之越权访问总结
  • LeetCode 2761 和等于目标值的质数对
  • Anywhere文章精读
  • c# 如何利用redis存储对象,并实现快速查询
  • 实时显示符合条件的完整宋词
  • 基于 DeepSeek 与天地图搭建创新地理信息应用
  • STM32F103低功耗模式深度解析:从理论到应用实践(上) | 零基础入门STM32第九十二步
  • 使用ctags+nvim自动更新标签文件
  • 基于springboot汽车租赁系统
  • 【百日精通JAVA | SQL篇 | 第二篇】数据库操作
  • K8S集群搭建 龙蜥8.9 Dashboard部署(2025年四月最新)
  • 云计算:数字化转型的核心引擎
  • 硬件工程师零基础入门教程(三)
  • 淘天集团Java开放岗暑期实习笔试(2025年4月2日)
  • 数据结构B树的实现
  • 3D Mapping秀制作:沉浸式光影盛宴 3D mapping show
  • Linux | I.MX6ULL内核及文件系统源码结构(7)
  • Java 基础-30-单例设计模式:懒汉式与饿汉式