数据结构与算法-数组排序算法

0. 交换两个变量中的值(辅助函数)

最简单的,利用一个临时变量:

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

还有其他办法么?答案是使用异或(XOR^)。但是为什么呢?

假设a为二进制数01b为二进制数10a^b的结果为11并将其存储在变量c中。

11^01=10
11^10=01
// 转换成我们的变量
c^a = b;
c^b = a;

我们发现,将两数异或的结果与其中一数再进行异或,可以得到另一个数。

原理:两数异或的结果保存了两个数上每一个二进制位不同或相同的信息。如果相应的二进制位不同,就标志为1,如果相同,则标志为0。

ac进行异或,相当于把c中和a相同的部分抵消掉,就可以得到b

bc进行异或,相当于把b中和a相同的部分抵消掉,就可以得到a

void swap(int *a, int *b)
{
    int c = *a ^ *b;
    *a = c ^ *a;
    *b = c ^ *b;
}

但代码上看着还是需要一个临时变量?

简单分析一下,就可以发现,c中信息丰富,可以说保存了ab的信息,我们可以把a变成c,信息是没有损失的。

void swap(int *a, int *b)
{
    *a = *a ^ *b; // 相当于c,保存了a和b的信息
    *b = *a ^ *b; // 相当于c^b,可以得到a
    *a = *a ^ *b; // 虽然看着是a^b,但现在a=c,b=a,实际是c^a,可以得到b
}

当然,如果你很骚(想装逼),也可以写得更简洁:

void swap(int *a, int *b)
{
    *a^=*b^=*a^=*b;
}

1. 冒泡排序

核心思想:

  • 把最后的位置留给最大元素。下次循环的时候就可以少遍历一个数。

  • 通过不断交换,把较大的数往后挪。

过程:

  1. 从头开始遍历,结束遍历的时候,末尾的就是最大的元素。
  2. 下一个最大元素只需要从n-1个数字中确定,再从n-2个数字中确定,...,最后到0。
  3. 当前元素 > 后一个元素,则交换两个元素位置,最大元素就后移了。
    • 也就是说,每次遍历较大的数字一定会逐渐后移。但遇到更大的数时,更大的数开始后移,所以最后一定是最大的数放在最后。
void BubbleSort(int *arr, int n)
{
    for (int i = n - 1; i > 0; i--) // 预留位置索引
        for (int j = 0; j < i; j++) // 当前元素和后一个元素比较,预留索引的比较通过j+1实现
            if (arr[j] > arr[j+1]) // 当前元素 > 后一个元素,则交换两个元素位置,最大元素就后移了。
                swap(&arr[j], &arr[j+1]);
}

2. 选择排序

核心思想:

  • 把最前面的位置留给最小元素。下次循环的时候就可以少遍历一个数。

  • 找到目标索引,最后进行交换。

过程:

  1. 从当前位置开始到最后一个元素,找到最小的元素索引。
  2. 找到最小元素的索引,如果和当前位置不一样,就交换两个元素。
void SelectionSort(int *arr, int n)
{
    int i, j, idx;
    for (i = 0; i < n - 1; i++) { // 预留位置索引
        // 1.从当前位置开始到最后一个元素,找到最小的元素索引
        idx = i;
        for (j = i + 1; j < n; j++) // 当前位置不用比较,从下一个位置开始
            if (arr[j] < arr[idx])
                idx = j;
        // 2.找到最小元素的索引,如果和当前位置不一样,就交换两个元素
        if (idx != i)
            swap(&arr[i], &arr[idx]);
    }
}

2.1 比较冒泡和选择排序

都是预留一个位置给目标元素,但是又有些不同。

  • 冒泡排序会不断交换元素。

  • 选择排序只会在最后判断是否需要交换

当然预留位置是最前还是最后都可以实现对应的算法。

比如冒泡算法,如果预留的位置是最前也可以做,但可能就不叫冒泡算法了。

冒泡算法:水中底部压力大,气泡较小,随着气泡上升,压力变小,气泡变大。

void BubbleSort2(int *arr, int n)
{
    for (int i = 0; i < n - 1; i++) // 预留位置索引
        for (int j = n - 1; j > i; j--) // 当前元素和前一个元素比较,结束位置为预留位置的后一个位置
            if (arr[j] < arr[j-1]) // 当前元素 < 前一个元素,则交换两个元素位置,最小元素就前移了。
                swap(&arr[j], &arr[j-1]);
}

同样,选择排序也可以预留最后的位置。

void SelectionSort2(int *arr, int n)
{
    int i, j, idx;
    for (i = n - 1; i > 0; i--) { // 预留位置索引
        // 1.从最后一个元素开始向前查找,找到最大的元素索引
        idx = i;
        for (j = i - 1; j >= 0; j--) // 当前位置不用比较,从前一个位置开始
            if (arr[j] > arr[idx])
                idx = j;
        // 2.找到最大元素的索引,如果和当前位置不一样,就交换两个元素
        if (idx != i)
            swap(&arr[i], &arr[idx]);
    }
}

3. 插入排序

类似于链表的头插法,我们把数组分为已排序部分和未排序部分。

你可以想象你正在摸扑克牌,你现在手中有一张拍,然后继续摸牌,摸牌的同时对你手中的牌进行排序(插入到合适的位置)。

  • 第一张牌,索引0不需要排序。
  • 继续摸牌,找到合适的位置i,将i后的牌后移。(给这张牌滕出位置)
  • 插入摸到的这张牌。
void InsertionSort(int *arr, int n)
{
    int i, j, tmp;
    for (i = 1; i < n; i++) { // 第一张牌,索引0不需要排序,直接跳过从1开始
        tmp = arr[i]; // 摸到的下一张牌,即未排序部分的开头
        for (j = i; j >= 1 && arr[j-1] > tmp; j--) // 从后往前遍历,将更大的牌后移(前面的是排序过的,是从小到大排列的)
            arr[j] = arr[j-1];
        arr[j] = tmp; // 循环将比摸到的牌更大的牌后移了,结束的位置就是插入牌的位置
    }
}

4. 希尔排序

其实就是分治法升级版的插入排序,又称为缩小增量排序。我们不断减半增量,直到增量为1时,退化为普通插入排序。

插入排序过程中:

  • 如果新摸到的是较大的牌,不需要滕位置,或者只需要滕很少的位置,就行了。
  • 如果新摸到的是较小的牌,每次要滕位置的时候,只能一个位置一个位置地挪,是非常低效的。

希尔排序通过增量进行了分组(分治),比较的是arr[j-increment]arr[j],跨度是increment,如果摸到的是较小的牌,只需要移动1次,而插入排序需要移动increment次。

  • 也就是说希尔排序的优势是,能让较小的牌更容易来到数组的前面部分,节约了移动次数。
void ShellSort(int *arr, int n)
{
    int i, j, tmp, increment;
    for (increment = n/2; increment > 0; increment /= 2) { // 增量变化
        for (i = increment; i < n; i++) { // 增量是increment,不同增量分组时,第一张摸到的牌
            tmp = arr[i]; // 摸到的下一张牌,即未排序部分的开头
            for (j = i; j >= increment && arr[j - increment] > tmp; j -= increment) // 从后往前遍历,将更大的牌后移(前面的是排序过的,是从小到大排列的)
                arr[j] = arr[j - increment]; 
            arr[j] = tmp; // 循环将比摸到的牌更大的牌后移了,结束的位置就是插入牌的位置
        }
    }
}

increment等于1的时候就和插入排序一模一样了。

需要注意的是:

由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

5. 堆排序

堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。

堆是具有以下性质的完全二叉树

每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。

或者

每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

如下图:

大顶堆、小顶堆

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:

数组表示

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆: arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆: arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

堆排序的基本思想是:

  1. 用待排序序列构造一个大顶堆
  2. 将其与末尾元素进行交换,此时末尾就为最大值。
  3. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
  4. 如此反复执行,便能得到一个有序序列了。

我们可以发现其实堆排序还是一种选择排序,用一句话概括思想:

利用堆结构特性,不断选出最大值,放到最后。

// 左儿子索引
#define LeftChild(i) (2 * (i) + 1)

/// 为目标节点找到合适的位置
/// @param arr 数组
/// @param i 当前要调整的节点索引
/// @param n 节点总数
void FormatHeap(int *arr, int i, int n) {
    int child, tmp;
    tmp = arr[i]; // 目标节点
    for (; LeftChild(i) < n; i = child) { // 从当前节点开始,不断遍历子节点
        child = LeftChild(i); // 获取左儿子
        // 左儿子如果是n-1就没有右儿子了,右儿子如果比左儿子大,就选右儿子
        if (child != n-1 && arr[child + 1] > arr[child])
            child++;
        // 如果子节点比目标节点大,则子节点上移覆盖父节点,滕出位置给目标节点,否则结束排序
        if (tmp < arr[child])
            arr[i] = arr[child];
        else
            break;
    }
    arr[i] = tmp; // 当前节点填入目标节点
}

void HeapSort(int *arr, int n) {
    int i;
    // 通过可能有值的父节点n/2-1开始,倒序遍历所有子节点
    // 也就是说,先排好下层节点,再排上层节点,所以FormatHeap的内遍历深度只会向下1层
    for (i = n/2 - 1; i >= 0; i--)
        FormatHeap(arr, i, n);
    // 构建好大顶堆后开始排序
    for (i = n-1; i > 0; i--) { // 预留位置索引(最后)
        swap(&arr[0], &arr[i]); // 将堆顶的最大元素,和预留位置元素交换
        // 现在只有栈顶的元素没有被放到合适的位置,所以是0
        // 预留位置已经填入了合适的值,所以总的个数现在是预留位置索引
        FormatHeap(arr, 0, i);
    }
}

6. 归并排序

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。

将问题成一些小的问题然后递归求解,而的阶段则将分的阶段得到的各答案"修补"在一起。

归并排序

进行合并时,我们需要一个额外的数组来进行辅助排序,再回填回原数组。

6.1 递归写法

/// 治,缝合,把结果存入b数组
/// @param a 数组a
/// @param b 数组b
/// @param lb 左边开始的索引
/// @param center 中间的索引
/// @param rEnd 结束的索引
void Merge(int *a, int *b, int lb, int center, int rEnd)
{
    int i, rb, count;
    i = lb; // 遍历开始
    rb = center + 1; // 右边开始的索引
    count = rEnd - lb + 1; // 缝合需要缝合的总数
    while (lb <= center && rb <= rEnd) // 把两个部分按大小存入临时数组b
        if (a[lb] < a[rb])
            b[i++] = a[lb++];
        else
            b[i++] = a[rb++];
    while (lb <= center) // 如果左半部分还有剩,则全部接到数组b后面
        b[i++] = a[lb++];
    while (rb <= rEnd) // 如果右半部分还有剩,则全部接到数组b后面
        b[i++] = a[rb++];
    for (int i = 0; i < count; i++, rEnd--) // 将b中的结果,回填到数组a中
        a[rEnd] = b[rEnd];
}

/// 分治
/// @param a 数组a
/// @param b 数组b
/// @param l 开始索引
/// @param r 结束索引
void RecursionMergeSort(int *a, int *b, int l, int r)
{
    if (l < r) { // 递归结束条件,结束时每个元素都变成单个的了
        int center = (l + r) >> 1 // 找到中心位置
        RecursionMergeSort(a, b, l, center); // 分成左半部分
        RecursionMergeSort(a, b, center + 1, r); // 分成右半部分
        Merge(a, b, l, center + 1, r); // 合并
    }
}

void MergeSort(int *arr, int n)
{
    int *b = malloc(n * sizeof(int)); // 临时数组,用于存放缝合结果
    if (b) {
        RecursionMergeSort(arr, b, 0, n-1);// 分治
        free(b);
    }
    else
        FatalError("No space for temp array!")
}

6.2 循环写法

/// 治,缝合,把结果存入b数组
/// @param a 数组a
/// @param b 数组b
/// @param lb 左边开始的索引
/// @param center 中间的索引
/// @param rEnd 结束的索引
void Merge(int *a, int *b, int lb, int center, int rEnd)
{
    int i, rb, count;
    i = lb; // 遍历开始
    rb = center + 1; // 右边开始的索引
    count = rEnd - lb + 1; // 缝合需要缝合的总数
    while (lb <= center && rb <= rEnd) // 把两个部分按大小存入临时数组b
        if (a[lb] < a[rb])
            b[i++] = a[lb++];
        else
            b[i++] = a[rb++];
    while (lb <= center) // 如果左半部分还有剩,则全部接到数组b后面
        b[i++] = a[lb++];
    while (rb <= rEnd) // 如果右半部分还有剩,则全部接到数组b后面
        b[i++] = a[rb++];
    for (int i = 0; i < count; i++, rEnd--) // 将b中的结果,回填到数组a中
        a[rEnd] = b[rEnd];
}

void LoopMergeSort(int *a, int *b, int n)
{
    int i, l, center, r, size;
    size = 1; // 每组里面的元素数量
    while (size < n) {
        for (i = 0; i < n;) {
            l = i; // 左边开始索引
            center = l + size - 1; // 中点索引
            r = center + size; // 右边结束索引
            r = r < n - 1 ? r : n - 1; // 右边部分可能没有那么多
            Merge(a, b, l, center, r); // 合并两个部分
            i = r + 1; //下一组的开始索引
        }
        size *= 2; // 合并的总大小翻倍
    }
}

void MergeSort(int *arr, int n)
{
    int *b = malloc(n * sizeof(int)); // 临时数组,用于存放缝合结果
    if (b) {
        LoopMergeSort(arr, b, n);
        free(b);
    }
    else
        FatalError("No space for temp array!")
}

7. 快速排序

快速排序是对冒泡排序的一种改进

基本思想:

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

排序过程:

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。
    • 此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

7.1 递归写法

int QuickSortKeyIndex(int *arr, int left, int right)
{
    int i = left; // 开始的索引
    int j = right; // 结束的索引
    int key = arr[left]; // 分界值
    while (i < j) { // 把比分界值大的放右边,比分界值小的放左边
        while (i < j && key <= arr[j]) //从右往左,跳过比分界值大的部分
            j--;
        arr[i] = arr[j]; // 把比分界值小的值放到i对应的索引
        while (i < j && key >= arr[i]) //从左往右,跳过比分界值小的部分
            i++;
        arr[j] = arr[i]; // 把比分界值大的值放到j对应的索引
    }
    arr[i] = key; // 在组内找完一遍以后就把分界值key回归
    return i;
}

void RecursionQuickSort(int *arr, int left, int right)
{
    if (left < right) { // 递归结束条件
        int i = QuickSortKeyIndex(arr, left, right); // 确定分界值的位置,然后分界值不再参与排序
        RecursionQuickSort(arr, left, i - 1); // 分为左半部分再来一遍
        RecursionQuickSort(arr, i + 1, right); // 分为右半部分再来一遍
    }
}

void QuickSort(int *arr, int n) {
        RecursionQuickSort(arr, 0, n-1);
}

7.2 循环写法

利用栈结构辅助,将栈帧记录的数据,存入自己的栈结构。

int QuickSortKeyIndex(int *arr, int left, int right)
{
    int i = left; // 开始的索引
    int j = right; // 结束的索引
    int key = arr[left]; // 分界值
    while (i < j) { // 把比分界值大的放右边,比分界值小的放左边
        while (i < j && key <= arr[j]) //从右往左,跳过比分界值大的部分
            j--;
        arr[i] = arr[j]; // 把比分界值小的值放到i对应的索引
        while (i < j && key >= arr[i]) //从左往右,跳过比分界值小的部分
            i++;
        arr[j] = arr[i]; // 把比分界值大的值放到j对应的索引
    }
    arr[i] = key; // 在组内找完一遍以后就把分界值key回归
    return i;
}

void LoopQuickSort(int *arr, int n)
{
    int top = -1;
    int *stack = (int *)malloc(sizeof(int) * (n + 4));
    stack[++top] = 0;
    stack[++top] = n - 1;
    int left, i, right;
    while (top > -1) {
        right = stack[top--];
        left = stack[top--];
        i = QuickSortKeyIndex(arr, left, right);
        if (left < i-1){
            stack[++top] = 0;
            stack[++top] = i-1;
        }
        if (i+1 < right) {
            stack[++top] = i+1;
            stack[++top] = right;
        }
    }
}

void QuickSort(int *arr, int n) {
    LoopQuickSort(arr, n);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容