七大基于比较的排序算法基本原理及实现(Java版)

目录

常见的排序算法:

一、直接插入排序

1、 实现思路:

2、 代码实现:

3、 特性总结:

二、希尔排序

1、 实现思路:

2、 代码实现:

3、 特性总结:

三、选择排序

1、 实现思路:

2、 代码实现:

3、 特性总结:

四、堆排序

1、 实现思路:

2、 代码实现:

3、 特性总结:

五、冒泡排序

1、 实现思路:

2、 代码实现:

3、 特性总结:

六、快速排序

1、 基本思想:

2、 代码实现:

1、 Hoare版

2、 挖坑法

3、前后指针法

4、快速排序优化

5、快速排序非递归版

3、 特性总结:

七、归并排序

1、 实现思路:

2、 代码实现:

1、递归版

2、非递归版

3、 特性总结:


常见的排序算法:

(以下排序方法都按从小到大升序排列为例)

一、直接插入排序

1、 实现思路:

直接插入排序比较简单,但是它用途很广泛,其基本实现思路为:

从给定数组的1下标开始,在该下标的左边找比它小的数据,找到后就把小的数据往前移,找完一个下标的,再找另一个下标的。

举个栗子:

给定一个序列array{1,5,3,4,7,2,3},按照直接插入排序的思路把它变为一个升序序列。

如果 j 下标的值大于 i 下标的值,那么就让 j 下标的值往后退一下,即array[j+1] = array[ j ]

当 j 再往前走,发现 j 下标的值小于 i 下标的值,那么 j 停止移动,说明 j 之前已经是排好序的了,此时我们把,存放 i 的值的tmp赋给 j + 1下标,就完成了小的数字前移,大的数字后移:

 经过不停地循环,直到……

此时, i 走到数组最后时又找到了一个 3 ,继续刚才的循环,看看这个 3 究竟会被放在哪里:

我们发现,3 被放在了 3 的后面,在排序前,3 也位于 3 的后面,而此时数组的排序已经完成了,那么我们就称这个直接插入排序是一个稳定的排序算法

稳定性:如果在一个待排序的序列中,有多个相同的元素,在经过排序后,这些元素在序列中的位置相对不变,则称这个排序算法是稳定的;否则为不稳定的。

2、 代码实现:

public static void insertSort(int[] array){
     for (int i = 1; i < array.length; i++) {
        int j = i - 1;
        int tmp = array[i];
        for (; j >= 0 ; j--) {
            if (array[j] > tmp){
                array[j+1] = array[j];
            }else {
                break;
            }
        }
        array[j+1] = tmp;
    }
}

3、 特性总结:

根据直接插入排序的时间复杂度,我们可以得出一个结论:

数据越有序,直接插入排序的速度就越快。

希尔排序应运而生。

二、希尔排序

1、 实现思路:

希尔排序的基本思想:对一个数组进行分组,然后对分组后的每组数据进行直接插入排序,不断地重复这个分组、排序的工作,当最后分为一组排序后,这组数据就成为了有序数组。

Q1:为什么要分组排序呢?

A1:因为分组可以加快排序的速度。

假设我们现在有10000个待排序的数据:

Q2:如何分组?

Q3:两种分组有什么区别呢?

我们对两种分组中的每组数据都进行直接插入排序,得到以下结果:

可以发现,通过希尔排序分组的数据,在排序之后,较大的数字都会放在后边,较小的数字都会放在前边,数组逐渐趋于有序

而一般情况分组的数据在经过排序后,不一定会达到上述效果。

所以我们选择第二种分组方式。

希尔排序又叫做缩小增量排序,这个增量指的就是每次所分的组的数量,当这个增量不断减小,数组就会不断趋于有序,当增量为1时,再进行一次排序,数组就会变得有序。

那么问题来了~

Q4:如何确定增量?

A4:

        希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列。

——严蔚敏《数据结构(C语言版)》

目前没有一个准确的确定增量的方法,你可以选择gap = n / 2,或者gap = n / 3,或者是其他的,只要让增量逐渐减小到1就行(gap表示增量,n表示数组长度)。

希尔排序本身就是对直接插入排序的一种优化排序,我们只要把刚才的直接插入排序稍微改动一下,即可实现希尔排序:

如果分组后的一组数据内不止2个元素,有多个元素呢?也是同理:

先比较一次红色组的数据,再比较一次绿色组的数据,只要我们每次让 j  -= gap ,那么每次比较时,每组数据就不会互相影响:

2、 代码实现:

private static void shell(int[] array, int gap){
        for (int i = gap; i < array.length; i++) {
            int j = i - gap;
            int tmp = array[i]; //把i的值先存起来
            for (; j >= 0; j -= gap) {
                if (array[j] > tmp) {
                    array[j + gap] = array[j];//此时需要交换的是j和j+gap下标的值
                } else {
                    break;
                }
            }
            array[j + gap] = tmp;//把tmp的值赋给j+gap下标
        }
    }
    public static void shellSort(int[] array){
        int gap = array.length / 2; //自己确定gap的取值
        while (gap > 1){
            shell(array,gap); //每次分组后都进行一次直接插入排序
            gap /= 2;
        }
        shell(array,gap);//最后一次排序
    }

3、 特性总结:

由于增量gap没有一个标准的取值方法,所以希尔排序的时间复杂度是不固定的。

三、选择排序

1、 实现思路:

选择排序又叫直接选择排序,它的实现思路非常简单:每次从待排序的数据中选择最小值(或最大值)放到序列的起始位置,直到全部待排序的数据排完。

2、 代码实现:

public class SelectSort {
    public static void swap(int[] array,int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    public static void selectSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i + 1; j < array.length ; j++) {
                if(array[j] < array[minIndex]){
                    minIndex = j;
                }
            }
            swap(array,i,minIndex);
        }
    }
}

上述代码中,每次遍历待排序的序列只找到了最小值的下标,如果每次遍历既可以找到最小值的下标,又可以找到最大值的下标,那么排序速度就会变快很多:

public class SelectSort {
    public static void swap(int[] array,int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void selectSort2(int[] array){
        int left = 0;
        int right = array.length - 1;
        while (left < right){
            int minIndex = left;//记录最小值的下标
            int maxIndex = left;//记录最大值的下标
            for (int i = left; i < right; i++) {
                if(array[i] < array[minIndex]){
                    minIndex = i;
                }
                if(array[i] > array[maxIndex]){
                    maxIndex = i;
                }
            }
            //最小值移动到序列前段
            swap(array,left,minIndex);
            //判断left处是否为最大值的下标
            if(left == maxIndex){
                maxIndex = minIndex;
            }
            //最大值移动到序列末端
            swap(array,right,maxIndex);
            left++;
            right--;
        }
    }
}

当 maxIndex 和 left 相同时,需要将 maxIndex 指向 minIndex (因为前一步的 swap()方法把 left 和 minIndex 的值进行了交换):

3、 特性总结:

时间复杂度O(N^2)
空间复杂度O(1)
稳定性不稳定

四、堆排序

1、 实现思路:

(1)创建一个大根堆(降序创建小根堆);

(2)将堆顶元素(最大值)和待排序元素中的最后一个元素进行交换;

(3)交换完成后向下调整。然后重复第二步,直到排序完所有元素。

2、 代码实现:

public class HeapSort {
    public static void swap(int[] array,int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    public void heapSort(int[] array){
        createBigHeap(array);
        int end = array.length - 1;
        while (end > 0){
            //交换堆顶元素和最后一个元素
            swap(array,0,end);
            //向下调整堆顶元素
            shiftDown(array,0,end);
            end--;
        }
    }
    //创建大根堆
    public void createBigHeap(int[] array){
        for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
            shiftDown(array,parent,array.length);
        }
    }
    public void shiftDown(int[] array,int parent,int len){
        int child = parent * 2 + 1;//左孩子节点的下标
        while (child < len){
            //判断是否有右孩子,并且右孩子的值是否大于左孩子的值
            if(child + 1 < len && array[child + 1] > array[child]){
                child += 1;
            }
            //判断较大的孩子节点的值和双亲节点的值的大小
            if(array[child] > array[parent]){
                swap(array,child,parent);
                //交换完成后需要判断后续的子堆是否为大根堆
                parent = child;
                child = parent * 2 + 1;
            }else {
                break;
            }
        }
    }
}

3、 特性总结:

时间复杂度        O(N*logN)
空间复杂度        O(1)
稳定性不稳定

五、冒泡排序

1、 实现思路:

冒泡排序的思路很容易理解:每次从0下标开始,把当前元素和下一个元素进行比较,如果当前元素大于它下一个元素,则进行交换,直到把最大的元素换到待排序序列的最后一个位置。

2、 代码实现:

public class BubbleSort {
    public static void swap(int[] array,int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    public void bubbleSort(int[] array){
        int len = array.length;
        for (int i = 0; i < len - 1; i++) {
            //i确定排序的趟数,一共有len个元素,最多排len - 1趟

            boolean flg = false;

            for (int j = 0; j < len - 1 - i; j++) {
                //j确定每趟排序需要比较的元素个数
                //每排一趟就减少一个比较的元素
                if(array[j] > array[j+1]){
                    swap(array,j,j+1);
                    flg = true;
                }
            }
            //如果flg为false,说明这趟排序中一对元素都没有进行交换
            //也说明当前序列已经是有序序列了,没必要再进行后续的比较了
            if(!flg){
                break;
            }
        }
    }
}

3、 特性总结:

时间复杂度O(N^2)
空间复杂度O(1)
稳定性稳定

六、快速排序

1、 基本思想:

(1)任取待排序元素中的某一个元素作为基准值

(2)将当前序列分割成两个子序列,左子序列的元素均小于基准值,右子序列的元素均大于基准值

(3)左右子序列重复上述两个步骤,直到待排序序列变为有序。

2、 代码实现:

将待排序序列按基准值划分为左右两个部分的常见方式有以下三种:

1、 Hoare版

主要思路:

(1)找基准值下标:从数组末端(right)往前找比数组首元素小的值,再从数组前端(left)往后找比数组首元素大的值,然后交换两个数值,right继续往前找,left继续往后找,直到二者相遇,最后把相遇位置的值和起始位置元素的值进行交换,相遇位置就是基准值所在的下标。

(2)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。

public class QuickSort {
    public void swap(int[] array,int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    //找基准值下标
    public int getPivot(int[] array,int left, int right){
        //记录left的起始位置
        int temp = left;
        //记录基准值
        int key = array[left];
        while (left < right){
            //从右边找比基准值小的元素的下标
            while (left < right && array[right] >= key){
                right--;
            }
            //从左边找比基准值大的元素的下标
            while (left < right && array[left] <= key){
                left++;
            }
            //交换
            swap(array,left,right);
        }
        //left和right相遇点即基准值下标
        //交换起始位置元素和基准值
        swap(array,left,temp);
        return left;
    }

    public void hoare(int[] array,int start, int end){
        //递归终止条件
        if(start >= end){
            return;
        }
        //获取基准值下标
        int pivot = getPivot(array,start,end);
        //递归基准值左子序列
        hoare(array,start,pivot-1);
        //递归基准值右子序列
        hoare(array,pivot+1,end);
    }

    public void sort(int[] array){
        hoare(array,0, array.length - 1);
    }
}

2、 挖坑法

主要思路:

(1)找基准值下标:将当前序列的其实元素存起来,然后从序列末端(right)找比当前序列起始位置小的元素,找到后,把该元素移到序列前端(left)位置,再从序列前端往后找比起始位置大的元素,找到后,把该元素移到right位置。

(2)重复循环上述步骤,直到left和right相遇,相遇点就是基准值下标,把起始位置的元素放到基准值下标处。

(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。

public class QuickSort2 {
    public void swap(int[] array,int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    //找基准值下标
    public int getPivot(int[] array,int left, int right){
        //存放起始位置元素
        int key = array[left];
        while (left < right){
            //从后往前找比key小的元素
            while (left < right && array[right] >= key){
                right--;
            }
            //找到后放到left位置
            array[left] = array[right];
            //从前往后找比key大的元素
            while (left < right && array[left] <= key){
                left++;
            }
            //找到后放到right位置
            array[right] = array[left];
        }
        //left和right相遇位置即基准值下标
        //把起始位置元素放到基准值下标处
        array[left] = key;
        return left;
    }

    public void hole(int[] array, int start, int end){
        //递归终止条件
        if(start >= end){
            return;
        }
        //获取基准值下标
        int pivot = getPivot(array,start,end);
        //递归基准值左子序列
        hole(array,start,pivot-1);
        //递归基准值右子序列
        hole(array,pivot+1,end);
    }

    public void sort(int[] array){
        hole(array,0, array.length - 1);
    }
}

3、前后指针法

主要思路:

(1)刚开始时,定义prev指针指向序列起始位置,定义cur指针指向prev的后面一个位置,curprev同时向后一步一步走,当cur遇到比起始位置元素大的值时,prev这次不往后走,cur往后走,当cur再次遇到比起始位置元素小的值时,交换prevcur位置的元素。

(2)当cur遍历完整个序列后,prev所在位置即基准值下标,交换序列起始位置的元素和基准值下标的元素。

(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。

public class QuickSort3 {
    public void swap(int[] array,int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    //找基准值下标
    public int getPivot(int[] array,int left, int right){
        //存放起始位置元素
        int key = array[left];
        int prev = left;
        int cur = left + 1;
        while (cur <= right){
            //cur向后找比key大的值
            if(array[cur] < key && array[++prev] != array[cur]){
                swap(array,prev,cur);
            }
            cur++;
        }
        //遍历完整个数组后,prev所在的位置就是基准值下标
        //交换起始位置元素和基准值下标的元素
        swap(array,prev,left);
        return prev;
    }
    public void doublePointer(int[] array, int start, int end){
        //递归终止条件
        if(start >= end){
            return;
        }
        //获取基准值下标
        int pivot = getPivot(array,start,end);
        //递归基准值左子序列
        doublePointer(array,start,pivot-1);
        //递归基准值右子序列
        doublePointer(array,pivot+1,end);
    }
    public void sort(int[] array){
        doublePointer(array,0, array.length - 1);
    }
}

4、快速排序优化

上述三种快速排序的方式,在针对一个有序序列进行排序时,时间复杂度会达到O(N^2),并且,如果序列内的元素过多,递归的深度就会非常深,可能会导致栈溢出,因此,我们需要对上述三种方式进行优化。

(1)三数取中法选基准值

上述三种快速排序的方式,在进行第一次递归时,都是使用了0下标的值来作为初始的基准值,所以在针对有序序列进行排序时,时间复杂度会达到O(N^2),所以,只要我们不使用0下标作为初始基准值即可减少时间复杂度。

三数取中法是从待排序序列的首元素、最后一个元素和中间位置的元素,三个元素中选择一个数值第二大的元素和首元素进行交换,这样就可以在针对有序序列进行排序时,出现时间复杂度为O(N^2)的情况。


    //找第二大的数的下标
    public int getMidNumIndex(int[] array,int start,int end){
        //中间元素的下标
        int mid = (start + end) / 2;
        
        int num1 = array[start];
        int num2 = array[mid];
        int num3 = array[end];

        if(num1 > num2){
            if(num1 > num3){
                if(num2 > num3){
                    return mid;
                }else {
                    return end;
                }
            }else {
                return start;
            }
        }else {
            //num1 <= num2
            if(num2 > num3){
                if(num1 > num3){
                    return start;
                }else{
                    return end;
                }
            }else {
                return mid;
            }
        }
    }

找到第二大的元素后,再把它和首元素进行交换即可:

    public void hole(int[] array, int start, int end){
        //递归终止条件
        if(start >= end){
            return;
        }

        //交换首元素和第二大的元素
        int midNumIndex = getMidNumIndex(array,start,end);
        swap(array,start,midNumIndex);

        //获取基准值下标
        int pivot = getPivot(array,start,end);
        //递归基准值左子序列
        hole(array,start,pivot-1);
        //递归基准值右子序列
        hole(array,pivot+1,end);
    }

(2)递归到元素数量少的子区间时,可以考虑使用插入排序 

假设在最好的情况,每次排序都可以通过基准值均分左右子序列,如果将每个子序列看做是一个节点,那么递归的所有结果可以看作一棵完全二叉树:

元素数量越少,递归的次数越多,时间复杂度也就越高,但是此时的子序列已经是趋于有序了,因此我们可以在元素数量较少的时候,使用插入排序来对剩余的元素进行排序,这样就可以大大减少时间复杂度。

    public void insertSort(int[] array, int start, int end){
        for (int i = start + 1; i <= end; i++) {
            int temp = array[i];
            int j = i - 1;
            for (; j >= start; j--) {
                if(array[j] > temp){
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j + 1] = temp;
        }
    }
    public void hole(int[] array, int start, int end){
        //递归终止条件
        if(start >= end){
            return;
        }
        if(end - start + 1 < 8){
            //子序列的元素小于8个的时候使用直接插入排序
            insertSort(array,start,end);
            return;
        }
        //交换首元素和第二大的元素
        int midNumIndex = getMidNumIndex(array,start,end);
        swap(array,start,midNumIndex);

        //获取基准值下标
        int pivot = getPivot(array,start,end);
        //递归基准值左子序列
        hole(array,start,pivot-1);
        //递归基准值右子序列
        hole(array,pivot+1,end);
    }

5、快速排序非递归版

为了彻底解决栈溢出的问题,我们可以使用非递归版的快速排序:

主要思路:

(1)借助一个栈或者队列,把基准值的左、右子序列的起始位置和结束位置分别入队;

(2)每次弹出两个元素作为新区间的起始位置和结束位置,再次找基准值,循环第一步操作;

(3)当栈内为空时结束循环。

public class QuickSort {
    //找基准值下标
    public int getPivot(int[] array,int left, int right){
        //存放起始位置元素
        int key = array[left];
        while (left < right){
            //从后往前找比key小的元素
            while (left < right && array[right] >= key){
                right--;
            }
            //找到后放到left位置
            array[left] = array[right];
            //从前往后找比key大的元素
            while (left < right && array[left] <= key){
                left++;
            }
            //找到后放到right位置
            array[right] = array[left];
        }
        //left和right相遇位置即基准值下标
        //把起始位置元素放到基准值下标处
        array[left] = key;
        return left;
    }

    //非递归版快速排序
    public void quickSort(int[] array,int start,int end){
        Stack<Integer> stack = new Stack<>();
        int pivot = getPivot(array,start,end);
        int left = start;
        int right = end;

        if(left < pivot - 1){
            stack.push(left);
            stack.push(pivot - 1);
        }
        if(right > pivot + 1){
            stack.push(pivot + 1);
            stack.push(right);
        }

        while (!stack.empty()){
            right = stack.pop();
            left = stack.pop();
            pivot = getPivot(array,left,right);
            if(left < pivot - 1){
                stack.push(left);
                stack.push(pivot - 1);
            }
            if(right > pivot + 1){
                stack.push(pivot + 1);
                stack.push(right);
            }
        }
    }
}

3、 特性总结:

时间复杂度(最好)时间复杂度(最坏)空间复杂度(最好)空间复杂度(最坏)稳定性
O(N*logN)O(N^2)O(logN)O(N)不稳定

七、归并排序

1、 实现思路:

(1)根据序列长度,将序列均分为左子序列和右子序列;

(2)对第一步进行递归,直到子序列中只有一个元素时进行合并;

(3)合并时,归并排序的问题就会被转换为对两个有序数组进行排序的简单问题。

2、 代码实现:

1、递归版

public class MergeSort {
    //分解
    public void mergeSort(int[] array,int left,int right){
        //终止条件
        if(left >= right){
            return;
        }

        int mid = (left + right) / 2;

        //1.分解左边
        mergeSort(array,left,mid);
        //2.分解右边
        mergeSort(array,mid + 1,right);
        //3.合并
        merge(array,left,right,mid);
    }
    
    //合并
    public void merge(int[] array,int start,int end,int midIndex){
        //记录左子序列的起始位置
        int ret = start;
        //创建一个临时数组
        int[] temp = new int[end - start + 1];
        //临时数组的下标
        int k = 0;
        //右子序列的起始位置
        int start2 = midIndex + 1;

        //两个子序列中都有元素的情况:
        while (start <= midIndex && start2 <= end){
            if(array[start] <= array[start2]){
                temp[k++] = array[start++];
            }else {
                temp[k++] = array[start2++];
            }
        }
        //只有一个子序列有元素的情况:
        while (start <= midIndex){
            temp[k++] = array[start++];
        }
        while (start2 <= end){
            temp[k++] = array[start2++];
        }

        //把temp数组的内容拷贝到array数组中
        for (int i = 0; i < temp.length; i++) {
            array[ret + i] = temp[i];
        }
    }
}

2、非递归版

非递归版的归并排序省去了分解的过程,直接开始合并:

public class MergeSort {
    public void mergeSort(int[] array){
        //gap表示每个子序列的元素个数
        int gap = 1;

        while (gap <= array.length){
            for (int i = 0; i < array.length; i += gap*2) {
                //左子序列起始位置
                int s1 = i;
                //左子序列结束位置
                int e1 = i + gap - 1;
                //右子序列结束位置
                int e2 = e1 + gap;
                //如果e2超过数组长度就越界了
                if(e2 < array.length){
                    merge(array,s1,e2,e1);//合并
                }
            }
            gap *= 2;
        }
    }
    //合并
    public void merge(int[] array,int start,int end,int midIndex){
        //记录左子序列的起始位置
        int ret = start;
        //创建一个临时数组
        int[] temp = new int[end - start + 1];
        //临时数组的下标
        int k = 0;
        //右子序列的起始位置
        int start2 = midIndex + 1;

        //两个子序列中都有元素的情况:
        while (start <= midIndex && start2 <= end){
            if(array[start] <= array[start2]){
                temp[k++] = array[start++];
            }else {
                temp[k++] = array[start2++];
            }
        }
        //只有一个子序列有元素的情况:
        while (start <= midIndex){
            temp[k++] = array[start++];
        }
        while (start2 <= end){
            temp[k++] = array[start2++];
        }

        //把temp数组的内容拷贝到array数组中
        for (int i = 0; i < temp.length; i++) {
            array[ret + i] = temp[i];
        }
    }
}

3、 特性总结:

时间复杂度O(N*logN)
空间复杂度O(N)
稳定性稳定

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吃点橘子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值