排序是算法中有着很重要的地位。它会涉及到很多方面的知识,不仅仅是算法相关的知识,还有很多很数据结构相关的知识,所以了解排序,对于我们学习算法和数据结构都是很有帮助的。
其中常见的排序有其中排序方法,分别为冒泡排序、选择排序、插入排序、希尔排序、堆排序、归并排序、快速排序,本文则是围绕着着其中排序方法进行讲解。
1、冒泡排序
冒泡排序(Bubble Sort)因其易于理解,作为不少初学者学习到的第一个的排序算法。
冒泡排序之所以叫为冒泡,是因为其想泡泡一样,一步步地将大的泡泡向上冒出,在数据中表现则为将较大的数字一步步交换到无序空间的后面。
1.1、排序过程图
1.2、排序思想
1.依次比较每两个相邻的数的大小,将较大的数交换到后面。
2.当第一次整个数据比较和交换完成后,整个数组中最大的数字则被排在数组的最后。
3.此时,已经将一个数排到了其相应的位置,不需要再移动了。
4.再次遍历整个数组,将次大的数通过比较和交换,排在其相应的位置。
5.重复上述流程,知道整个数组都被排序。
1.3、排序代码
public static void bubbleSort(int[] arr){
//因为排好数组中第二个数时,第一个数也自然排好了,所以遍历的此数为length - 1
for (int i = 0; i < arr.length - 1; i++) {
//第i + 1次遍历整个数组
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
}
}
//此时倒数第i + 1个数则排序好了
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
1.4、代码改进
上述的代码任然有一定改进的空间:若输入的数组已经完整排序,此时则不需要再排序,但上述的代码并不能实现这一点,于是可以加上一个对数组排序的判断。
public static void bubbleSort(int[] arr){
//isSorted 用于判断数组是否已经排序完成
boolean isSorted = true;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
//此时数组仍未排序完成
isSorted = false;
}
}
//若排序完成,则 isSorted 应该为true,则提前跳出循环
if (isSorted){
break;
}
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2、选择排序
选择排序(Selection Sort),顾名思义,就是每次遍历的时候,都选择一个最小(大)的数进行排序,直到此数排到相应的位置,结束此数的排序,再选除此数外最小(大)的数进行排序。
2.1、排序过程图
2.2、排序思想
1.初次遍历数组时,根据依次比较,筛选出当前数组中最小的数。
2.将此数通过交换到数组最前的位置,此数则排序完成。
3.再将除排序好的数以外的数组,再次筛选出最小值。
4.再次将当前最小值通过交换,排到数组的最前。
5.重复上述流程,直到整个数组排序完成。
2.3、排序代码
public static void selectSort(int[] arr){
//依次筛选并排序最小值
for (int i = 0; i < arr.length - 1; i++) {
//用min记录当前数组最小值的索引
int min = i;
//遍历数组,筛选出最小值得索引
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]){
min = j;
}
}
//交换最小值和当前数组最前的位置的值
swap(arr, min, i);
//此时当前数组最小值得排序完成
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2.4、代码改进——双向选择排序
在上述的代码,即单向选择排序中,一次遍历数组,只能筛选出一个数进行排序,效率略低,则可以考虑一次遍历筛选出两个数,即可以筛选出最小值和最大值。
2.4.1、改进排序思想
1.在每次遍历数组时,筛选出当前数组中的最小值和最大值。
2.将最小值和最大值分别交换到当前数组的最前和最后。
3.再将除已排序完的数的数组,再次按照上述流程进行排序。
注:
可能会出现max和low重合的情况,即max的值并未变化的情况,则此时min才是max应该指向的位置。
2.4.2、改进排序代码
public static void selectSortOP(int[] arr){
//记录最小值得索引
int low = 0;
//记录最大值的索引
int high = arr.length - 1;
while (low <= high){
//从左边开始遍历
int min = low;
int max= low;
for (int i = low + 1; i <= high; i++) {
//筛选出更小的数
if (arr[i] < arr[min]){
min = i;
}
//筛选出更大的值
if (arr[i] > arr[max]){
max = i;
}
}
//交换数组最左边的值和最小值
swap(arr, min, low);
//max和low重合的情况
if (max == low){
max = min;
}
//交换数组最右边的值和最大值
swap(arr, max, high);
//移动左右指针
++low;
--high;
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3、插入排序
插入排序(Insertion Sort)是排序算法中稳定的排序算法,它不会改变相同数字之间的相对位置,而且这种排序算法, 和选择排序有点类似,若已理解了选择排序,那插入排序的理解会变得相对简单。插入排序的主要思想,是将无序区间里的数依次插入到有序区间里的对应位置,并不能改变有序区间的有序性。
3.1、排序过程图
3.2、排序思想
1.在排序时,有序区间为[ 0 , i ),无序区间为[ i , arr.length ),注意两区间都是左闭右开的区间。
2.选择当前无序区间的第一个数,和前面的数依次比较,若前面的数比当前的数大,则交换,直到遇到遇到一个小于它的数,或已经到达数组的最前位置。
3.此时,排好了一个数,当前的有序区间为[ 0 , i + 1),[ i - 1, arr.length )。
4.并继续按照上述流程比较和交换,直到整个数据排序完成。
3.3、排序代码
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) {
swap(arr, j, j - 1);
}
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3.4、代码改进——折半插入排序
上述的代码在查找其相应的位置时,效率略微低下,于是可选择常用的二分查找,来帮助我们找到其应该移动到的位置,提高效率。
3.4.1、改进代码思想
1.首先仍然是要找到当前无序区间中的第一个数。
2.通过二分查找,找到其应该移动到的位置。
3.将有序数组从其应该移动到的位置开始,到数组末尾的数,全部向后搬移一个位置。
4.将数字直接插入到相应位置。
3.4.2、改进代码
public static void insertSortBS(int[] arr){
// 有序区间[0..i)
// 无序区间[i...n]
for (int i = 1; i < arr.length; i++) {
int left = 0;
int right = i;
int val = arr[i];
//使用二叉查找,找到应插入到的位置。
while (left < right){
int mid = (left + right) >> 1;
if (val < arr[mid]){
right = mid;
} else {
left = mid + 1;
}
}
// 搬移left..i的元素
for (int j = i; i > left; i--){
arr[j] = arr[j - 1];
}
// left就是val插入的位置
arr[left] = val;
}
}
4、希尔排序
希尔排序(Shell Sort)又称缩小增量法。它的思想和插入排序的思想有一定的关联。插入排序是将整个数组看成一个整体,而希尔排序是将整个数组划分为几大块,先每块分别进行插入排序,再将几个大块拆分为更多的小块,并继续上述的操作,直到拆分到每块都只有一个元素,此时整个数组则排序完成。
4.1、排序流程图
4.2、排序思想
1.先将整个数组对半拆分,得到两部分数组。
2.对那两部分数组进行插入排序。
3.再将每块进行对半拆分,得到几部分的数组。
4.再次对那几部分的数组进行插入排序。
5.直到每部分都只有一个元素,此时已无法再拆分,则整个数组都排序完成。
4.3、排序代码
public static void shellSort(int[] arr){
//首次拆分
int gap = arr.length >> 1;
while (gap > 1){
//进行插入排序
insertSortByGap(arr, gap);
//对每部分进行拆分
gap >>= 1;
}
//此时整个数组已经近乎有序,再次使用插入排序即可有序
insertSort(arr);
}
//此函数为将插入排序中的起始索引1,改为gap
private static void insertSortByGap(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
//对每部分的数组进行插入排序
for (int j = i; j >= gap && arr[j] > arr[j - gap]; j -= gap) {
swap(arr, j, j - gap);
}
}
}
5、堆排序
堆排序(Heap Sort),顾名思义,就是要用到堆的知识。其基本的原理也就是选择排序,知识不再使用遍历的方式查找无序区间的最大数,而是用堆来来选择无序区间中的最大值。但是需要注意的是,排升序需要用到最大堆,排降序需要用到最小堆。
5.1、排序流程图
5.2、排序思想
在讲解堆排序思想前,需要先知道一些关于堆排序的知识:
1.堆在逻辑上事一颗完全二叉树。
2.堆在物理上是保存在数组中(按照层序遍历顺序保存)。
3.最大堆:每一个节点的值都大于或等于其两个子节点的值。
4.最大堆的下沉操作:为满足最大堆的性质,需要将指定位置的数在二叉树的逻辑上,向下交换到合适的位置。
5.最大堆的构建:从层序遍历中最后一个非叶子节点开始,每个节点执行下沉操作,则可完成二叉树的构建。
在堆排序中,需要用到以上的有关于堆的知识,此时来讲解堆排序:
1.将无序数组构建为最大堆的形式,此时堆顶的数就是当前无序数组中最大的数。
2.将无序数组的最后一个数和第一个数交换,则最大的数被交换到了无序数组的最后,成为了有序数组的一部分。
3.将交换后的第一个数(也就是刚刚无序数组的最后一个数)进行下沉操作,以继续满足最大堆的性质。
4.重复上述的操作,直到整个数组排序完成。
5.3、排序代码
public static void heapSort(int[] arr){
//从最后一个非叶子节点开始下沉操作,构建最大堆
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--){
siftDown(arr, i, arr.length);
}
//对数组进行交换和下沉操作
for (int i = arr.length - 1; i > 0; i--){
swap(arr, 0, i);
siftDown(arr, 0, i);
}
}
//下沉操作
private static void siftDown(int[] arr, int i, int length) {
//保证此节点有左节点
while (2 * i + 1 < length){
//左节点的索引
int j = (i << 1) + 1;
//判断是否有右节点,并筛选出两节点的最大值所在的索引
if (j + 1 < length && arr[j + 1] > arr[j]){
++j;
}
//若节点的值小于节点的最大值,则结束循环
if (arr[i] > arr[j]){
break;
}
//交换两节点的值,并更改当前节点的索引
swap(arr, i, j);
i = j;
}
}
6、归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
6.1、排序过程图
6.2、排序思想
归并排序的主要思想就是先分割,再合并。
1.将整个数组折半分割,直到每个子数组都只有一个数。
2.再将每两个相邻的子数组结合。
3.创建一个新数组,来暂时保存数组。
4.一次比较两子数组中的值,将较小的值依次放入新数组中。
5.再将剩下未放入到新数组中的数,依次放入到新数组中。
6.当整个新数组都放满时,再用新数组的值覆盖掉之前数组的值。
3.不停地结合并排序,直到整个数组合并完成,则排序完成。
6.3、排序代码
public static void mergeSort(int[] arr){
//数组的区间为闭区间
mergeSortInternal(arr, 0, arr.length - 1);
}
private static void mergeSortInternal(int[] arr, int l, int r) {
//求中间索引
int mid = r + ((l - r) >> 1);
//将左数组排序
mergeSortInternal(arr, l, mid);
//将右数组排序
mergeSortInternal(arr, mid + 1, r);
//若数组已排序成功,则返回
//若数组还为排序成功,则将左右数组合在一起归并排序
if (arr[mid] > arr[mid + 1]){
merge(arr, l, mid, r);
}
}
private static void merge(int[] arr, int l, int mid, int r) {
//创建新数组,来暂时保存数组
int[] newArr = new int[r - l + 1];
//左数组的第一个索引
int i = l;
//右数组的第一个索引
int j = mid + 1;
//辅助数组的第一个索引
int k = 0;
//依次比较出较小的数,并放入到新数组中
while (i <= mid && j <= r){
if (arr[i] <= arr[j]){
newArr[k++] = arr[i++];
} else {
newArr[k++] = arr[j++];
}
}
//前数组有剩余的数
while (i <= mid){
newArr[k++] = arr[i++];
}
//后数组有剩余的数
while (j <= r){
newArr[k++] = arr[j++];
}
//将辅助数组的值赋给原数组
for (int m = 0; m < newArr.length; m++) {
arr[l + m] = newArr[m];
}
}
6.4、代码改进——解决可能的栈溢出问题
6.4.1、代码改进思想
归并排序在执行时,需要多次调用函数,而当数据过多时,可能会出现栈溢出的问题,于是,可以在数组长度较短时,使用插入排序(在近乎有序的情况下,插入排序的效率高),来解决栈溢出的问题。
6.4.2、改进代码
public static void mergeSort(int[] arr){
//数组的区间为闭区间
mergeSortInternal(arr, 0, arr.length - 1);
}
private static void mergeSortInternal(int[] arr, int l, int r) {
//若当前数组偿付较短时,使用插入排序,否则容易栈溢出
if (r - l <= 15){
insertSort(arr, l, r);
return;
}
//求中间索引
int mid = r + ((l - r) >> 1);
//将左数组排序
mergeSortInternal(arr, l, mid);
//将右数组排序
mergeSortInternal(arr, mid + 1, r);
//若数组已排序成功,则返回
//若数组还为排序成功,则将左右数组合在一起归并排序
if (arr[mid] > arr[mid + 1]){
merge(arr, l, mid, r);
}
}
private static void merge(int[] arr, int l, int mid, int r) {
//创建新数组,来暂时保存数组
int[] newArr = new int[r - l + 1];
//左数组的第一个索引
int i = l;
//右数组的第一个索引
int j = mid + 1;
//辅助数组的第一个索引
int k = 0;
//依次比较出较小的数,并放入到新数组中
while (i <= mid && j <= r){
if (arr[i] <= arr[j]){
newArr[k++] = arr[i++];
} else {
newArr[k++] = arr[j++];
}
}
//前数组有剩余的数
while (i <= mid){
newArr[k++] = arr[i++];
}
//后数组有剩余的数
while (j <= r){
newArr[k++] = arr[j++];
}
//将辅助数组的值赋给原数组
for (int m = 0; m < newArr.length; m++) {
arr[l + m] = newArr[m];
}
}
//插入排序
public static void insertSort(int[] arr,int l,int r){
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
swap(arr, j - 1, j);
}
}
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
7、快速排序
快速排序(Quick Sort)是排序算法中很重要,而且也是使用很广泛的一种排序算法。它的思想也归并排序有点类似,都是需要分区,并对每个分区进行排序,然后对整个分区进行排序。
7.1、排序流程图
7.2、排序思想
1.先对整个数组进行分区。
2.在分区时便可以对数组进行一定程度的排序。
3.从待排序的区间中选择一个数,作为目前整个区间的基准值。
4.遍历整个待排序区间,将比基准值小的(可以包括相等的)放在基准值的左边,将比基准值打的(也可以包括相等的)放到基准值的右边。
5.采用分治的思想,对左右的两个小区间也使用同样的方式处理,直到小区间的长度为1,代表已经有序,或者小区间的长度为0,代表没有数据。
6.重复上述的流程,直到整个数组排序完成。
7.3、排序代码
public static void quickSort(int[] arr){
//对数组进行快速排序,区间为闭区间
quickSortInternal(arr, 0, arr.length - 1);
}
//对数组分区
private static void quickSortInternal(int[] arr, int l, int r) {
//将当前的数组进行快速排序
int p = partition(arr, l, r);
//对左区间进行快速排序
quickSortInternal(arr, l, p);
//对右区间进行快速排序
quickSortInternal(arr, p + 1, r);
}
//对分好区的子数组进行快速排序
private static int partition(int[] arr, int l, int r) {
//用v来记录基准值
int v = arr[l];
//j为小于基准值的区间
int j = l;
for (int i = l + 1; i <= r; i++) {
//若小于基准值,则将数字移动到小于的区间中
if (arr[i] < v){
swap(arr, i, ++j);
}
}
//将基准值交换到相应的位置
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
7.4、代码改进1——防止栈溢出和加入随机数
7.4.1、代码改进思想
1.上述的代码在较少的数据处理时,一般不会有什么问题,但当遇到很多的数据时,就可能会出现栈溢出的问题。于是可以在处理较少的数据时,直接使用插入排序(插入排序在近乎有序时,效率很高),减少对函数的调用。
2.在进行快速排序选择基准值时,可以使用随机数,使得排序会更加有效。
7.4.2、改进代码
public static void quickSort(int[] arr){
quickSortInternal(arr, 0, arr.length - 1);
}
//对数组分区
private static void quickSortInternal(int[] arr, int l, int r) {
//在较少的数据时,使用插入排序,
if (r - l <= 15){
insertSort(arr, l, r);
return;
}
int p = partition(arr, l, r);
quickSortInternal(arr, l, p);
quickSortInternal(arr, p + 1, r);
}
//对分好区的子数组进行快速排序
private static int partition(int[] arr, int l, int r) {
//加入随机数,使得此次的排序会更加有效
int randomIndex = random.nextInt(l, r);
swap(arr, l, randomIndex);
int v = arr[l];
int j = l;
for (int i = l + 1; i <= r; i++) {
if (arr[i] < v){
swap(arr, i, ++j);
}
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void insertSort(int[] arr,int l,int r){
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
swap(arr, j - 1, j);
}
}
}
7.5、代码改进2——二路快排
7.5.1、代码改进流程图
7.5.2、代码改进思想
之前的代码都是一次遍历只能选择一个数放入到对应的区间,于是我们便可以想到是否可以一次遍历,选出两个数来放入到对应的区间,以提高遍历的效率
1.使用左右两个指针,分别指向待排序数据的起始和结尾的位置,左指针向右遍历,右指针向左遍历。
2.当左指针找到一个大于基准值的数,且右指针找到第一个小于基准值的数时,两数据进行交换。
3.重复上述的流程,直到两指针相遇,则遍历结束。
4.将基准值交换到相应的位置。
7.5.3、改进代码
public static void quickSort2(int[] arr){
quickSortInternal2(arr, 0, arr.length - 1);
}
private static void quickSortInternal2(int[] arr, int l, int r) {
if (l - r <= 15){
insertSort(arr, l, r);
return;
}
int p = partition2(arr, l, r);
quickSortInternal2(arr, l, p);
quickSortInternal2(arr, p + 1, r);
}
private static int partition2(int[] arr, int l, int r) {
int randomIndex = random.nextInt(l, r);
swap(arr, l, randomIndex);
int v = arr[l];
//左指针
int m = l;
//右指针
int n = r;
while (m < n){
//左指针一直遍历,直到找到一个大于基准值的数
while (m < n && arr[m + 1] < v){
++m;
}
//右指针一直遍历,直到找到一个小于基准值的数
while (m < n && arr[n - 1] > v){
--n;
}
//交换左右指针所指向的数
swap(arr, m, n);
++m;
--n;
}
swap(arr, l, m);
return m;
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void insertSort(int[] arr,int l,int r){
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
swap(arr, j - 1, j);
}
}
}