那些年,那些排序

关键词助手:排序,冒泡排序,快速排序(hoare版、挖坑法、前后指针法),快速排序非递归,选择排序,堆排序,插入排序,希尔排序,归并排序,计数排序。

目录

交换类排序

冒泡排序

快速排序 

hoare法

挖坑法

前后指针法

非递归实现

 插入类排序

插入排序

 希尔排序

 选择类排序

选择排序

 堆排序

其他排序

 归并排序

递归实现 

非递归实现

计数排序


 

交换类排序

冒泡排序

这应该是大多数人排序算法的启蒙算法,因为它算是所有排序算法中最简单易懂的一种。

算法思想:

反复扫描待排序序列,在扫描的过程中顺次比较相邻的两个数据元素的大小,若与预期逆序,就交换两个元素的位置。

32642218518962401328
32642218518962401328
32226418518962401328
32221864518962401328
32221851648962401328
32221851648962401328
32221851646289401328
32221851646240891328
32221851646240138928
32221851646240132889

 以上为序列{32,64,22,18,51,89,62,40,13,28}一次扫描的结果。每一行高光处为该次扫描时顺次比较的两个元素。

冒泡排序的C语言实现:

void Swap(int* p1, int* p2) {
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

void BubbleSort(int* a, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int k = 0; k < n - i - 1; k++) {
            if (a[k] > a[k + 1]) {
                Swap(&a[k], &a[k + 1]);
            }
        }
    }
}

对每一次扫描后的数组进行打印

快速排序 

 某种意义上说,快速排序可以算得上是排序算法中比较难理解的一类排序了,但是由于它与冒泡排序同属于交换类排序,所以就放在这里进行介绍吧。

算法思想:

从待排序序列中选择一个元素作为枢轴,其值为key,将序列分为两部分,一部分存储小于key的元素,一部分存储大于key的元素,最后将枢轴插入与两部分之间。

对于枢轴两边的部分分别进行上述操作。

32642218518962134028
22181328326451896240
221813
132218
2218
1822
326451896240
324064518962
64518962
51626489
6489
13182228324051626489

以上为序列{32,64,22,18,51,89,62,40,13,28}的部分快速排序操作过程。我们可以很直观地看到。通过对将序列划分为具有某些特性的块,我们可以很快地使一个无序的数组有序化。但由以上步骤可以看出,如果待排序数组本身就的“熵”很小,接近有序,那么其复杂度会极大的升高。

为了解决这样的问题,我们通常会对于快速排序进行一些简单的优化——从待排序的‘块’中随机地挑选三个元素,取这三个元素地中值来作为本阶段排序地枢轴。以此来最大可能地保证枢轴取值地合理性。

三数取中法

void mid(int* a, int left, int right) {
    int mid = (left + right) / 2;
    int* i = &a[left];
    int* j = &a[right];
    int* k = &a[mid];
    if (a[left] > a[right]) {
        if (a[mid] > a[left]) {
            Swap(&a[left], &a[right]);
        }
        else {
            if (a[right] < a[mid]) {
                Swap(&a[mid], &a[right]);
            }
        }
    }
    else {//a[left] <= a[right]
        if (a[left] > a[mid]) {
            Swap(&a[left], &a[right]);
        }
        else {
            if (a[mid] < a[right]) {
                Swap(&a[mid], &a[right]);
            }
        }
    }
}

对于快速排序地实现方法,现在普遍有三种方法——hoare法挖坑法前后指针法。而对于这三种方法的具体实现又有递归非递归两种方法。对此,我将重点举例递归的实现方法。

hoare法

简单的描述就是,从待排序序列两端分别设置start,end两个指针。

start指针向后遍历,遇到大于基准值(枢轴)的元素则停止,end向前遍历,遇到小于基准值的元素start指针则停止。

如果start不等于end,那么交换两者所指向位置的元素。

否则,将基准值与两指针共同指向的元素交换。

PS.表格实例中并没有使用三数取中法。 

32642218518962134028
32642218518962134028
13642218518962324028
13642218518962324028
13182264518962324028
13182264518962324028
13182228518962324064
int PartSort1(int* a, int left, int right) {
    if (left >= right) {
        return 0;
    }
    mid(a, left, right);
    int key = a[right];
    int start = left;
    int end = right;
    while (start < end) {
        while (a[start] < key) {
            start++;
        }
        while (a[end] >= key) {
            end--;
        }
        if (start < end) {
            Swap(&a[start], &a[end]);
        }
    }
    Swap(&a[start], &a[right]);
    PartSort1(a, left, start - 1);
    PartSort1(a, start + 1, right);
    return 0;
}

挖坑法

与hoare法类似的是,挖坑法中我们依旧会使用startend两个指针分别从待排序序列两端开始遍历。

同时,通过一个变量key来存储基准值——我们通常以序列首或者序列尾的元素来作为基准值,即start或end指针最初指向的位置。

将基准值(此处以序列尾,即end初始指向为例)的位置看做‘坑’,令start指针向后遍历,遇到大于或等于基准值的元素则停止,将之填入‘坑’中,形成新的‘坑’。

移动end指针寻找小于基准值的元素填坑。

如此往复,直到start等于end,将基准值填入坑中。

 PS.表格实例中并没有使用三数取中法。

key = 2832642218518962134028
key = 2832642218518962134032
key = 2813642218518962134032
key = 2813642218518962644032
key = 2813182218518962644032
key = 2813182228518962644032
int PartSort2(int* a, int left, int right) {
    if (left >= right) {
        return 0;
    }
    mid(a, left, right);
    int end = right;
    int start = left;
    int key = a[right];
    while (start < end) {
        while (start != end && a[start] < key) {
            start++;
        }
        a[end] = a[start];
        while (start != end && a[end] >= key) {
            end--;
        }
        a[start] = a[end];
    }
    a[end] = key;
    PartSort2(a, left, end - 1);
    PartSort2(a, end + 1, right);
    return 0;
}

前后指针法

与前两种方法不同,前后指针法通过pre与cur两个指针一前一后,分别代表当前遍历到的位置以及大于基准值部分与小于基准值部分的界限。

pre寻找着小于基准值的元素,cur指向第一个大于基准值的元素。

根据pre与cur之间是否存在pre==cur+1的关系,来决定是否需要交换。

 PS.表格实例中并没有使用三数取中法。

curpre
32642218518962134028
32642218518962134028
18642232518962134028
18132232518962644028
18132228518962645032

int PartSort3(int* a, int left, int right) {
    if (left >= right) {
        return 0;
    }
    mid(a, left, right);
    int cur = left;
    int pre = cur - 1;
    int key = a[right];
    while (cur < right) {
        if (a[cur] < key && ++pre != cur) { //此处利用了&&的短路效应.
            Swap(&a[cur], &a[pre]);
        }
        cur++;
    }
    Swap(&a[++pre], &a[right]);
    PartSort3(a, left, pre - 1);
    PartSort3(a, pre + 1, right);
    return 0;
}

非递归实现

 对于快速排序的非递归实现,我们需要引入栈。

void QuickSortNonR(int* a, int left, int right) {
    //挖坑
    stack* s = StackInit();
    StackPush(s, left);
    StackPush(s, right);
    while (!StackEmpty(s)) {
        right = StackPull(s);
        left = StackPull(s);
        if (left >= right) {
            continue;
        }
        mid(a, left, right);
        int end = right;
        int start = left;
        int key = a[right];
        while (start < end) {
            while (start != end && a[start] < key) {
                start++;
            }
            a[end] = a[start];
            while (start != end && a[end] >= key) {
                end--;
            }
            a[start] = a[end];
        }
        a[end] = key;
        StackPush(s, left);
        StackPush(s, end - 1);
        StackPush(s, end + 1);
        StackPush(s, right);
    }
}

 插入类排序

插入排序

将待排序序列的首端看作一个有序序列,将与该有序序列紧邻的元素通过比较插入序列中适当的位置。

32642218518962134028
32642218518962134028
22326418518962134028
18223264518962134028
18223251648962134028
18223251648962134028
18223251626489134028
13182232516264894028
13182232405162648928
13182228324051626489
13182228324051626489
void InsertSort(int* a, int n) {
    for (int i = 1; i < n; i++) {
        for (int k = i; k > 0; k--) {
            if (a[k] < a[k - 1]) {
                Swap(&a[k], &a[k - 1]);
            }
            else {
                break;
            }
        }
    }
}

 

 希尔排序

直接插入排序法,在待排序序列中元素较少且基本有序时算法性能最佳。

希尔排序又称为缩小增量排序法,是一种基于插入思想的排序方法,它利用了直接插入排序的最佳性质。

设置一个增量gap,将序列中下标差值为gap的元素作为一个子序列,如此将待排序序列分割成若干较稀疏的子序列,分别进行插入排序。

缩小gap的值,重复以上操作,直至gap为1时,此时只有一个子序列,对该序列进行直接插入排序,完成排序过程。

32642218518962134028
22133218402851646289
22133218402851646289
13182228324051626489
13182228324051626489

关于希尔排序的增量gap有多种取法,最初Shell提出过取gap=n/2,gap=gap/2的取法。

后来Kunth经过大量实验统计资料,提出了取gap=(gap/3)+1的取法。

除以上两种外,gap还有许多的取法,但都没有上述两种使用广泛,便不赘述了。

void ShellSort(int* a, int n) {
    int gap = n;
    while (gap > 1) {
        gap = gap / 3 + 1;
        for (int i = gap; i < n; i++) {
            int k = i - gap;
            int key = a[i];
            while (k >= 0 && a[k] > key) {
                a[k + gap] = a[k];
                k -= gap;
            }
            a[k + gap] = key;
        }
    }
}

 选择类排序

选择排序

选择排序的算法思想非常简单。

通过重复遍历待排序序列,每一次遍历都从待排序序列中锁定一个最小值(或最大值),将它们按放置便可以构成已排序序列。

32642218518962134028
13642218518962324028
13182264518962324028
13182264518962324028
13182228518962324064
13182228328962514064
13182228324062518964
13182228324051628964
13182228324051628964
13182228324051626489
13182228324051626489
void SelectSort(int* a, int n) {
    for (int i = 0; i < n; i++) {
        int key = i;
        for (int k = i; k < n; k++) {
            if (a[k] < a[key]) {
                key = k;
            }
        }
        Swap(&a[key], &a[i]);
    }
}

 堆排序

堆排序是选择排序算法的改进版本。

采用堆排序时,只需要一个记录大小的辅助空间。

堆排序是在排序过程中,将序列中存储的数据看作一个完全二叉树,利用完全二叉树双亲结点与子节点之间的内在关系来选择最小元素。即堆排序使用了二叉树的顺序结构特征进行分析,但其元素仍然是使用数组来存储的。

堆排序使用表格并不能直观的表现出其原理与思想,对于堆排序的理解难点也在于对数据结构中二叉树相关知识点的理解,在此便不以图表的形式来表现了,仅以以下文字作为解释,如果想要更加清楚的了解堆排序的知识,可以学习或回顾数据结构中二叉树相关知识。

 首先,将待排序序列所组成的完全二叉树调整为一个大顶堆/小顶堆——由此保证双亲结点一定大于/小于其孩子节点。

由二叉树的性质很容易知道,此时大顶堆/小顶堆的根节点是未排序序列中最大/最小的元素。

将根节点与最后一个叶节点进行交换,此时序列尾的元素可以看作已排序部分,所以,将待排序序列长度减去1,对于剩下的待排序序列再次进行调整,重复上述操作,最终可以得到一个完整的已排序序列。

堆排序与选择排序的最大不同在于,选择排序是通过一次次遍历来锁定序列中的最大/最小值的,而堆排序巧妙地运用了二叉树的大顶堆/小顶堆,通过对树的一次次调整,使得根节点成为我们想要筛选出来的元素。 

void AdjustDwon(int* a, int n, int root) {
    int child = root * 2 + 1;
    while (child < n) {
        if (child + 1 < n && a[child] > a[child + 1]) {
           child++; 
        }
        if (a[child] < a[root]) {
            Swap(&a[child], &a[root]);
            root = child;
            child = root * 2 + 1;
        } else {
            return;
        }
    }
}

void HeapSort(int* a, int n) {
    for (int i = (n - 2) / 2; i >= 0; i--) {
        AdjustDwon(a, n, i);
    }

    int end = n - 1;
    while(end > 0) {
        Swap(&a[end], &a[0]);
        AdjustDwon(a, end, 0);
        end--;
    }
}

其他排序

 归并排序

归并排序的核心思想是合并,将两个或两个以上的有序表合并为一个有序表。

假设待排序序列长度为n,首先将这n个元素看为n个子序列。此时每个子序列都是有序的。

 将这些子序列两两合并,得到新的一批有序子序列,如此重复,直到所有子序列合并完毕得到一个完整的有序序列。

 同一行不同的颜色代表着不同的子序列。

32642218518962134028
32641822518913622840
18223264135162892840
13182232516264892840
13182228324051626489

递归实现 

//两个有序序列合并为一个有序序列
void MergeDate(int* a, int head, int mid, int tail, int* tmp) {
    int i = head;
    int j = mid + 1;
    int k = head;
    while (i <= mid && j <= tail) {
        if (a[i] < a[j]) {
            tmp[k++] = a[i++];
        }
        if (a[i] >= a[j]) {
            tmp[k++] = a[j++];
        }
    }
    while (i <= mid) {
        tmp[k++] = a[i++];
    }
    while (j <= tail) {
        tmp[k++] = a[j++];
    }
    memcpy(a + head, tmp + head, sizeof(int) * (tail - head + 1));
}

void Merge(int* a, int head, int tail, int* tmp) {
    if (head >= tail) {
        return;
    }
    int mid = (tail + head) / 2;

    Merge(a, head, mid, tmp);
    Merge(a, mid + 1, tail, tmp);

    MergeDate(a, head, mid, tail, tmp);
}

void MergeSort(int* a, int n) {
    int* tmp = (int*)calloc(n, sizeof(int));
    int head = 0;
    int tail = n - 1;
    Merge(a, head, tail, tmp);
    free(tmp);
}

非递归实现

void MergeData(int* arr, int head, int mid, int tail, int* tmp) {
    int i = head;
    int j = mid + 1;
    int k = head;

    while (i <= mid && j <= tail) {
        if (arr[i] < arr[j]) {
            tmp[k++] = arr[i++];
        }
        else {
            tmp[k++] = arr[j++];
        }

    }
    while (i <= mid) {
        tmp[k++] = arr[i++];
    }
    while (j <= tail) {
        tmp[k++] = arr[j++];
    }
}



void MergeSortNor(int* arr, int size) {
    int* tmp = (int*)malloc(sizeof(int) * size);
    int gap = 1;

    while (gap < size) {
        for (int i = 0; i < size; i += 2 * gap) {
            int head = i;
            int tail = head + 2 * gap - 1;
            int mid = (head + tail) / 2;

            if (mid > size - 1) {
                mid = size - 1;
            }
            if (tail > size - 1) {
                tail = size - 1;
            }
            MergeData(arr, head, mid, tail, tmp);
        }

        memcpy(arr, tmp, sizeof(int) * size);
        gap *= 2;
    }
    free(tmp);
}

计数排序

计数排序是一种特殊的排序方法,它适用于取值范围不是很大的序列当中。某些时候,它的速度甚至可以快于O(nlogn)。

 1.8 计数排序 | 菜鸟教程 (runoob.com)

上方网址中的动图比简单的描述形象不少,感兴趣的可以看一看。

下面也会以表格进行简单的演示{1,2,4,2,1,8,9,5,0,4,3,6,5,7,6,4,5,6,3,9} 

排序前: 

12421895043657545639

计数 

个数1222333112
下标0123456789

排序后: 

01122334445556667899

 以下提供的是经过优化的,不会改变序列稳定性的计数排序。

void CountSort(int* arr, int size) {
    int min = arr[0];
    int max = arr[0];
    int* arr2 = (int*)malloc(sizeof(int) * size);

    for (int i = 1; i < size; i++) {
        if (max < arr[i]) {
            max = arr[i];
            continue;
        }

        if (min > arr[i]) {
            min = arr[i];
        }
    }

    int* count = (int*)calloc((max - min + 1), sizeof(int));

    for (int i = 0; i < size; i++) {
        count[arr[i] - min]++;
    }

    for (int i = 1; i < (max - min + 1); i++) {
        count[i] = count[i - 1] + count[i];
    }

    for (int i = size - 1; i >= 0; i--) {
        int tmp = --count[arr[i] - min];
        arr2[tmp] = arr[i];
    }
    free(count);
    for (int i = 0; i < size; i++) {
        arr[i] = arr2[i];
    }
    free(arr2);
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云雷屯176

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

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

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

打赏作者

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

抵扣说明:

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

余额充值