4.3 几种简单的排序算法

冒泡排序

冒泡排序的核心就是依次比较相邻的两个数,升序排序时将小数放在前面,大数放在后面。排序算法一般都需要进行多轮比较,以下是冒泡排序的升序比较过程。

第 1 轮:首先比较第 1 个和第 2 个数,将小数放前,大数放后;然后比较第 2 个数和第 3 个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后;至此第一轮结束,将最大的数放到了最后。

第 2 轮:仍从第一对数开始比较,将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的数),第二轮结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。

总结来说,第 1 到 n-1 轮中(n 为数组长度,下文同)第 i 轮的作用是把第 i 大的数放到数组的 n-i 下标处。按此规律操作,直至最终完成排序。由于在排序过程中总是将大数往后放,类似于气泡往上升,所以称作冒泡排序。

通过上面的分析可以看出,假设需要排序的序列的个数是 n,则需要经过 n-1 轮,最终完成排序。在第一轮中,比较的次数是 n-1 次,之后每轮减少 1 次。

在这里插入图片描述
在这里插入图片描述
冒泡排序示例动图(来源:https://www.cnblogs.com/onepixel/p/7674659.html)如下:
在这里插入图片描述

插入排序

本节介绍的最基础的插入排序算法。它将待排序的数据分为两个部分第一部分中的数据是已经排好序的(初始时,第一个数据划入第一部分)第二部分中的数据是无序的。之后,每次从第二部分取出头部(即第一个)元素,把它插入到第一部分的合适位置,使插入后的第一部分仍然有序。直接插入排序的具体流程如下:

第一轮:以下标 1 的元素(记为 a1,后面类似)为头部向前找插入位置,如果 a1 ≥a0 ,则维持现状;否则 a1 向前插(a0 后移,位置 0 处将 a1 放入)。

第二轮:以 a2 为头部向前找插入位置,如果 a2 ≥ a1 ,则维持现状;否则 a1 向后挪。继续与 a0 比较,此时如果 a2 ≥a0 ,则在位置 1 处放入 a2 ,如果 a0 > a2 ,将 a0 后挪一位,在位置 0 处将 a2 放入。

总的来说,第 i 轮时,ai 为待定位的数据,通过与前序数据比较并将更大的数据后挪(如有必要)的方式找到合适的位置插入 ai,使得数组的前 i+1 个元素有序。一共要进行 n-1 轮

代码:
在这里插入图片描述

插入排序示例动图(来源:https://www.cnblogs.com/onepixel/p/7674659.html)如下:

图片描述

快速排序

快速排序是对冒泡排序的一种改进,是通过每一趟排序,将要排序的数组(或后续讲解的集合)分割成两个独立的部分。其中,一部分的所有数据比另一部分的所有数据都要小。然后通过递归重复这种操作,对分割后的两部分数据分别进行快速排序,最终达到整个数据都是有序排列的。下面的章节将会介绍单向扫描法和双向扫描法两种方式的快速排序
存在两种方式:单项扫描,双向扫描

在这里插入图片描述

快速排序之单向扫描

  1. 选定数组的一个元素,将之称为“主元”。之后,扫描一趟数组,将大于或等于主元的元素放在主元的右边,把小于或等于主元的元素放在主元的左边,这个过程被称为用主元分割数组。具体做法是:

a. 选定数组的第一个元素(即 nums 数组中的元素 4)作为主元。

b. 定义两个标记变量 sp 和 bigger,它们都是数组下标。其中 sp 表示 1 中,在从左往右扫描一趟数组的过程中,当前正在扫描的位置,它会向右移动;bigger 是边界,其右侧的数据大于或等于主元。初始时,sp 指向数组的第二个元素,bigger 指向数组的最后一个元素,如图所示。

在这里插入图片描述

c. 假设数组名是 arr,第一趟的比较流程是:在 sp≤bigger 的情况下循环,比较 arr[sp]<=主元 是否成立,如果是,sp 右移一位;否则,就交换 arr[sp] 和 arr[bigger],并将 bigger 的位置左移一位(注意 sp 原地不动),第一次比较过程如图所示。
数据交换前:
在这里插入图片描述
数据交换后:
在这里插入图片描述
bigger 左移一位:
在这里插入图片描述
d. 继续循环,重复 c 中描述的过程,如下。

接"bigger 左移一位:"如图,因为当前的arr[sp]<=主元(1<4)成立,所以 sp 右移一位即 sp++;

此时 arr[sp]<=主元(6>4)不成立,所以交换 arr[sp] 和 arr[bigger],再将 bigger 左移一位,如下图所示。

数据交换前:
在这里插入图片描述
数据交换后:
在这里插入图片描述
bigger 左移一位:
在这里插入图片描述

因为arr[sp]<=主元(2 < 4)成立,sp 右移一位,如图所示。

sp 右移一位:
在这里插入图片描述

因为 arr[sp]<=主元(7>4)不成立,所以交换 arr[sp] 和 arr[bigger],再将 bigger 左移一位,如下图所示。

数据交换前:
在这里插入图片描述
数据交换后:
在这里插入图片描述
bigger 左移一位:
在这里插入图片描述

此时,sp==bigger,仍要继续判断,此处因为 arr[sp]<=主元(3<4)成立,所以 sp 还会右移一次变为大于 bigger,而 bigger 保持不动。

至此,循环结束,bigger 右侧的数据全部大于或等于主元,注意 bigger 本身指向的数据是小于主元的,下面通过交换 arr[bigger] 和主元就可以完成以主元分割数组的任务。

e. 交换 arr[bigger] 和主元,如图所示。
在这里插入图片描述

  1. 此时,主元左边的元素都小于主元,主元右边的元素都大于或等于主元。即主元已经是处在排好序的位置了。
  2. 将此时的数组以主元为界,分割为两部分,分别对两部分递归处理,即从a. 开始——选取新的主元(图中的新主元 1 和新主元 2),如图所示。

在这里插入图片描述

用同样的方法,将新的主元也放置到排好序的位置。

  1. 递归的结束条件是:子数组只有 1 个元素或者 0 个元素。
    在这里插入图片描述

快速排序之单向扫描代码

新建一个 TestQuickSort.java 文件,并提供类的代码:

public class TestQuickSort {
    //核心代码及main方法
}

排序算法中会用到很多的数据交换,所以我们先实现方法 swap(int[] arr, int index1, int index2) 实现对数组 arr 的 index1 和 index2 下标位置的元素实现调换:

//交换arr[index1]和arr[index2]
static void swap(int[] arr, int index1, int index2) {
    int tmp = arr[index1];
    arr[index1] = arr[index2];
    arr[index2] = tmp;
}

设计方法 pv(int[] arr, int l, int r) 方法实现一次单向扫描,返回边界的下标:

    //单向扫描的排序
    static int pv(int[] arr, int l, int r) {
        // 主元
        int p = arr[l];
        // 扫描指针
        int sp = l + 1;
        int bigger = r;
        while (sp <= bigger) {
            if (arr[sp] <= p)
                sp++;
            else {
                swap(arr,sp,bigger);
                bigger--;
            }
        }
        swap(arr,l,bigger);
        return bigger;
    }

完成快速排序算法 quickSort(int[] arr, int l, int r):

    //递归调用单向扫描方法
    static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            int q = pv(arr, l, r);
            quickSort(arr, l, q - 1);
            quickSort(arr, q + 1, r);
        }
    }

组合代码并进行测试

public class TestQuickSort {
    public static void main(String[] args) {
        int[] a = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
        System.out.println("排序前的数据为:");
        for (int a1 : a) {
            System.out.print(a1+" ");
        }
        quickSort(a, 0, 14);
        System.out.println("\n"+"排序后的数据为:");
        for (int a2 : a) {
            System.out.print(a2+" ");
        }

    }

    //单向扫描的排序
    static int pv(int[] arr, int l, int r) {
        // 主元
        int p = arr[l];
        // 扫描指针
        int sp = l + 1;
        int bigger = r;
        while (sp <= bigger) {
            if (arr[sp] <= p)
                sp++;
            else {
                swap(arr,sp,bigger);
                bigger--;
            }
        }
        swap(arr,l,bigger);
        return bigger;
    }

    //递归调用单向扫描方法
    static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            int q = pv(arr, l, r);
            quickSort(arr, l, q - 1);
            quickSort(arr, q + 1, r);
        }
    }

    //交换arr[index1]和arr[index2]
    static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }
}

编译、运行此程序,结果如下图所示。

在这里插入图片描述

至此我们已经完成单向扫描法实现快速排序。快速排序在理解起来比较复杂,需要仔细分析和研究。

双向扫描法

双向扫描法仍然是选取第一个元素为主元,然后在主元以外的元素里,从左右两侧同时扫描,如下图中的 left、right。

图片描述

left 在向右扫描(移动)的过程中,如果 arr[left]<=主元 成立,则 left 右移,否则停止移动;right 向左扫描的过程中,如果 arr[right]>=主元 成立,则 right 左移,否则停止移动。当 left 和 right 都停止移动时,如果这时 left<=right,则交换左右 arr[left] 和 arr[right],如下图所示。

扫描停止并准备交换数据:

图片描述

交换后的数据:

图片描述

之后继续向中间遍历,直到 left> right,如下。

a. 上一个图中,arr[left]<=主元 成立,所以 left 右移;arr[right]>=主元 成立,所以 right 左移,如图所示。

图片描述

此时,left<right,因此需要交换 arr[left]和 arr[right],如图所示。 right 左移数据交换前:

图片描述

right 左移数据交换后:

图片描述

b. 上一个图中,arr[left]<=主元成立,所以 left 右移;arr[right]>=主元成立,所以 right 左移,如图所示。

图片描述

此时,left<=right 成立,且 left 和 right 满足停止条件,因此需要交换 arr[left] 和 arr[right],如下图所示。 数据交换前:

图片描述

数据交换后:

图片描述

c. 上一个图中,arr[left]<=主元 成立,所以 left 右移;arr[right]>=主元 成立,所以 right 左移,如图所示。

图片描述

此时,已达到了循环退出条件 left> right,因此循环终止。

d. 再交换主元与 arry[right],如下图所示。

主元与 arry[right]交换前:

图片描述

主元与 arry[right]交换后:

图片描述

至此,主元也处在了合适的位置上,一趟排序结束。

再用递归,将主元左侧和右侧的子数组视为两个需要排序的数组,重复以上步骤即可实现对整个数组的排序。

在这里插入图片描述

双向扫描代码实现

双向扫描法原理与单向扫描法思想上相同,只是使用不同的划分策略,同单向扫描不同点就在于扫描方法及调用的参数上,接下来我们详细看一下。

新建一个 TestDoubleQuickSort.java 文件,补充类的代码:

public class TestDoubleQuickSort {
    //核心代码及main方法
}

我们直接展示双向扫描的核心代码:

设计方法 pv2(int[] arr, int l, int r) 方法实现一次双扫描,返回边界的下标:

static int pv2(int[] arr, int l, int r) {
int p = arr[l];
int left = l + 1;
int right = r;
while (left <= right) {
// left向右走,直到遇见大于主元的元素
while (left <= right && arr[left] <= p)
left++;
// right向左走,直到遇见小于或等于主元的元素
while (left <= right && arr[right] > p)
right–;
if (left < right) {
swap(arr, left, right);
}
}
// while退出时,left, right两者交错,且right指向最后一个小于等于主元的元素,
// 也就是主元应该在的位置
swap(arr, l, right);
return right;
}

补充完成方法调用等内容,完整代码如下:

public class TestDoubleQuickSort {
    public static void main(String[] args) {
        int[] a = {12,32,13,45,34,65,76,78,89,57};
        System.out.println("排序前的数据为:");
        for (int a1 : a) {
            System.out.print(a1+" ");
        }
        quickSort(a, 0, 9);
        System.out.println("\n"+"排序后的数据为:");
        for (int a2 : a) {
            System.out.print(a2+" ");
        }

    }

    static int pv2(int[] arr, int l, int r) {
        int p = arr[l];
        int left = l + 1;
        int right = r;
        while (left <= right) {
            // left向右走,直到遇见大于主元的元素
            while (left <= right && arr[left] <= p)
                left++;
            // right向左走,直到遇见小于或等于主元的元素
            while (left <= right && arr[right] > p)
                right--;
            if (left < right) {
                swap(arr, left, right);
            }
        }
        // while退出时,left, right两者交错,且right指向最后一个小于等于主元的元素,
        // 也就是主元应该在的位置
        swap(arr, l, right);
        return right;
    }

    //递归调用双指针扫描方法
    static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            int q = pv2(arr, l, r);
            quickSort(arr, l, q - 1);
            quickSort(arr, q + 1, r);
        }
    }

    //交换arr[index1]和arr[index2]
    static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }
}

编译、运行此程序,结果如下图所示。

在这里插入图片描述

快速排序已成为工业界的排序标准,因其易于实现且性能稳定。这里我们介绍排序算法的目的主要是加深对数组、方法和递归的理解,不准备深入探讨算法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值