排序算法 总结

排序的分类

稳定和非稳定

  • 稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
  • 非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。

原地排序

  • 指不申请多余的空间来进行的排序,就是在原来的排序数据中比较和交换的排序。
  • 属于原地排序的是: 希尔排序 、冒泡排序、插入排序、选择排序、堆排序、快速排序

比较和非比较

  • 常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

内排序和外排序

  • 内排序:指在排序期间数据对象所有存放在内存的排序。 大部分算法实现都是使用内排序
  • 外排序:指在排序期间所有对象太多,不能同一时候存放在内存中,必须依据排序过程的要求,不断在内,外存间移动的排序

排序算法总览

在这里插入图片描述

选择排序

步骤

  1. 首先,找到数组中最小的那个元素
  2. 其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么当然它就和自己交换)。
  3. 然后,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。
  4. 如此往复,直到将整个数组排序

算法分析

  • 时间复杂度为O(n^2)
  • 空间复杂度O(1)

其他

  • 是不稳定排序,举个例子:数组 6、7、6、2、8,在对其进行第一遍循环的时候,会将第一个位置的6与后面的2进行交换。此时,就已经将两个6的相对前后位置改变了。因此选择排序不是稳定性排序算法。

Java实现

public class SelectSort {
    public static void main(String[] args) {
        int[] a = {1, 4, 2, 8, 3, 7, 8, 4, 6, 9};
        System.out.println(Arrays.toString(a));

        for (int i = 0; i < a.length - 1; i++) {
            int min = i;
            for (int j = i; j < a.length; j++) {
                if (a[j] <= a[min])
                    min = j;
            }
            int temp = a[min];
            a[min] = a[i];
            a[i] = temp;
        }
        System.out.println(Arrays.toString(a));
    }
}

优化版本

  • 选择排序的时间复杂度是O(NN),不管是最好情况还是最坏情况,找最小数的过程都需要遍历一遍,所以,选择排序最好情况也是O(NN)
  • 如果在每一次查找最小值的时候,也可以找到一个最大值,然后将两者分别放在它们应该出现的位置,这样遍历的次数就比较少了
public class SelectSort {
    public static void main(String[] args) {
        int[] a = {1, 4, 2, 8, 3, 7, 8, 4, 6, 9};
        System.out.println(Arrays.toString(a));

//        selectSort(a);
        selectSortOptimized(a);

        System.out.println(Arrays.toString(a));
    }

    /**
     * 优化的选择排序
     */
    public static void selectSortOptimized(int[] a) {
        int left = 0, right = a.length - 1;
        int min, max;//存储最大最小值的下标
        while (left < right) {
            min = left;
            max = left;
            for (int i = left; i <= right; ++i) {
                if (a[i] < a[min]) min = i;
                if (a[i] > a[max]) max = i;
            }
            swap(a, left, min);
            if (left == max)
                max = min;//这里是考虑了最大值就在left下标的位置这种情况
            swap(a, right, max);
            ++left;
            --right;
        }
    }

    public static void swap(int[] arr, int front, int back) {
        int temp = arr[front];
        arr[front] = arr[back];
        arr[back] = temp;
    }
}

直接插入排序

步骤

  1. 从数组第2个元素开始抽取元素。
  2. 把它与左边相邻的第一个元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比它大的元素,然后插到这个元素的右边,其他元素依次后移。
  3. 继续选取第3,4,….n个元素,重复步骤 2 ,选择适当的位置插入。

算法分析

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

其他

  • 直接插入排序稳定吗? 稳定
  • 插入排序是一种比较简单直观的排序算法,适用处理数据量比较少或者部分有序的数据

Java实现

public class InsertSort {
    public static void main(String[] args) {
        int[] a = {1, 4, 2, 8, 3, 7, 8, 4, 6, 9};
        System.out.println(Arrays.toString(a));

        for (int i = 1; i < a.length; i++) {
            int j = i - 1;
            if (a[i] < a[j]) {
                int temp = a[i];
                do {
                    a[j + 1] = a[j];
                    j--;
                } while (j >= 0 && temp < a[j]);//注意这里temp不要写成了a[i]
                a[j + 1] = temp;//注意是j+1
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

鸡尾酒排序

  • 鸡尾酒排序是冒泡排序的优化算法
  • 鸡尾酒排序的优点是能够在特定条件下,减少排序的回合数;而缺点也很明显,就是代码量几乎增加了1倍
public class CockTailSort {
    public static void main(String[] args) {
        int[] arr = {9, 2, 3, 6, 4, 8, 1, 0, 5, 7};
        System.out.println(Arrays.toString(arr));

        cockTailSort(arr);

        System.out.println(Arrays.toString(arr));
    }

    public static void cockTailSort(int[] arr) {
        for (int i = 0; i < arr.length / 2; ++i){
            boolean isSorted = true;
            for (int j = i; j < arr.length - 1 - i; ++j) {
                if (arr[j] > arr[j + 1]){
                    swap(arr, j, j + 1);
                    isSorted = false;
                }
            }
            if (isSorted) break;
            isSorted = true;
            for (int j = arr.length - 1 - i; j > i; --j) {
                if (arr[j] < arr[j - 1]){
                    swap(arr, j, j -1);
                    isSorted = false;
                }
            }
            if (isSorted) break;
        }
    }

    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

二路归并排序(MergeSort)

简介

  • 二路归并排序是经典的排序算法,核心思想是分治,属于稳定排序
  • 二路归并的递归路径实质上是一个完全二叉树,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为log2n。因此总的平均时间复杂度为O(nlogn)
  • 而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)
  • 当然还有n路归并排序

算法实现

Java

public class MergeSort {
    public static void main(String[] args) {
        int[] a = {1,4,2,8,3,7,8,4,6,9};
        System.out.println(Arrays.toString(a));
        int[] temp = new int[a.length];//在外部声明辅助数组,避免在递归栈中声明
        mergeSort(a, 0, a.length - 1, temp);

        System.out.println(Arrays.toString(a));
    }
    public static void mergeSort(int[] arr, int left, int right, int[] temp){
        if (left >= right) return;
        int mid = (left + right) >> 1;
        mergeSort(arr, left, mid, temp);
        mergeSort(arr, mid+1, right, temp);
        int l = left, r = mid + 1;
        int t = 0;//辅助计数变量
        while (l <= mid && r <= right){
            if (arr[l] < arr[r]) temp[t++] = arr[l++];
            else temp[t++] = arr[r++];
        }
        //剩下的子序列添加到辅助空间中
        while (l <= mid) temp[t++] = arr[l++];
        while (r <= right) temp[t++] = arr[r++];
        //将辅助数组的值copy到原数组,注意copy到原数组的范围是left到right
        t = 0;
        while (left <= right) arr[left++] = temp[t++];
    }
}

考虑一种情况

  • 将代码改为如下所示:
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left >= right) return;
        int mid = (left + right) >> 1;
        System.out.println("left: " + left);
        System.out.println("right: " + right);
        mergeSort(arr, left, mid - 1, temp);
        mergeSort(arr, mid, right, temp);
        int l = left, r = mid;
        int t = 0;//辅助计数变量
        while (l <= mid - 1 && r <= right) {
            if (arr[l] < arr[r]) temp[t++] = arr[l++];
            else temp[t++] = arr[r++];
        }
        //剩下的子序列添加到辅助空间中
        while (l <= mid - 1) temp[t++] = arr[l++];
        while (r <= right) temp[t++] = arr[r++];
        //将辅助数组的值copy到原数组,注意copy到原数组的范围是left到right
        t = 0;
        while (left <= right) arr[left++] = temp[t++];
    }
  • 主要是改了这两行代码:
mergeSort(arr, left, mid - 1, temp);
mergeSort(arr, mid, right, temp);
  • 这样划分在逻辑上似乎没问题,但会造成栈内存溢出,假设left = 2, right = 3,那么mid = 2,这样就会一直递归调用mergeSort(arr, 2, 3, temp),造成内存溢出。
  • 所以不能这样写

参考

  • https://www.cnblogs.com/chengxiao/p/6194356.html

快速排序(QuickSort)

简介

  • 快排与傅里叶变换等算法并称为二十世纪十大算法
  • 使用了分治法
  • 冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。

算法分析

这样一共需要多少轮呢?

  • 最好情况:每次划分都产生两个长度差不多的子区间,也就是说,所取得基准都是当前无序区的中值元素,这样的递归树的高度为:

l o g 2 n log_2 n log2n

  • 而每一层划分的时间为n, 所以:

T n = O ( n l o g n ) , S ( n ) = O ( l o g 2 n ) (递归栈空间) Tn = O(nlogn),S(n) = O(log_2n)(递归栈空间) Tn=O(nlogn)S(n)=O(log2n)(递归栈空间)

  • 最坏情况:每次选取得基准都是当前无序区的最大(小)值,这样的话,递归树高度n,需要n-1次划分:

T n = O ( n 2 ) , S ( n ) = O ( n ) Tn = O(n^2) , S(n) = O(n) Tn=O(n2)S(n)=O(n)

  • 平均情况:

T n = O ( n l o g n ) Tn = O(nlogn) Tn=Onlogn

基准元素的选取

  • 基准元素,英文pivot。
  • 最简单的方式是选择数列的第一个元素,这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?时间复杂度退化为:

n 2 n^2 n2

  • 当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果

算法特性

  • 不稳定
  • 原地排序

算法实现

单边循环法

Java
  • 仅仅需要修改双边循环法的partition函数
public static int partition2(int[] arr, int front, int back){
    int pivot = front;
    int mark = front;//代表小于基准元素的区域边界
    for (int i = front+1; i <= back; i++){
        if (arr[i] < arr[pivot]){
            mark++;//因为找到一个小于基准元素的元素
            swap(arr, i, mark);
        }
    }
    swap(arr, pivot, mark);
    return mark;
}

双边循环法

C++
#include<iostream>
using namespace std;

void disppart(int *a, int f, int b){
	
	static int i = 1;
	cout << "第" << i << "次划分:" << endl;
	for(int j = 0; j < f; j++)
		cout << "  ";
	for(int j = f; j <= b; j++)
		cout << a[j] << " ";
	cout << endl; 
	i++;
}

int partition(int *a, int f, int b){
	int pivot = f;//pivot = a[f]
	int front = f, back = b;
	int temp = 0;
	while(1){	
		while(front < back && a[back]  >= a[pivot]) back--;//这两个while的顺序看似无关紧要,实际上是很关键的
		while(front < back && a[front] <= a[pivot]) front++;
        if(front < back){
			temp = a[front];
			a[front] = a[back];
			a[back] = temp;
		}	
		else 
			break;	
	}
	temp = a[pivot];
	a[pivot] = a[front];
	a[front] = temp;
	disppart(a, f, b);
	return back;	//return front 也行
}
void QuickSort(int *a, int f, int b){
	int mid = 0;	
	if(f < b){
		mid = partition(a, f, b);
		QuickSort(a, f, mid - 1);
		QuickSort(a, mid + 1, b);
	}
}

int main(){
	int a[10] = {6,8,7,9,0,1,3,2,4,5};
	QuickSort(a, 0, 9);
	for(int i = 0; i < 10; i++)
		cout << a[i] << " ";
	return 0;
} 
Java
import java.util.Arrays;

public class QuickSort {
	public static void main(String[] args) {
		int[] arr = {6,8,7,9,0,1,3,2,4,5};
//		System.out.println(Arrays.toString(arr));
		sort(arr, 0, 9);
		System.out.println(Arrays.toString(arr));
	}
	public static void sort(int[] arr, int front, int back) {
		int mid = 0;
		if(front < back) {
			mid = partition(arr, front, back);
			sort(arr, front, mid-1);
			sort(arr, mid+1, back);
		}
	}
	public static int partition(int[] arr, int front, int back) {
		int pivot = front;
		int f = front, b = back;
		while(true) {
			while(f < b && arr[b] >= arr[pivot]) b--;
			while(f < b && arr[f] <= arr[pivot]) f++;
			if(f < b) swap(arr, f, b);
			else break;
		}
		swap(arr, pivot, f);
		return f;
	}
	public static void swap(int[] arr, int front, int back) {
		int temp = arr[front];
		arr[front] = arr[back];
		arr[back] = temp;
	}
}

非递归方式

  • 和递归实现相比,非递归方式代码的变动只发生在quickSort方法中。该方法引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。
  • 每一次循环,都会让栈顶元素出栈,通过partition方法进行分治,并且按照基准元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环
Java
public static void quickSort2(int[] arr, int startIndex, int endIndex) {
    // 用一个集合栈来代替递归的函数栈
    Stack<Map<String, Integer>> quickSortStack = new Stack<>();
    // 整个数列的起止下标,以哈希的形式入栈
    Map<String, Integer> rootParam = new HashMap<>();
    rootParam.put("startIndex", startIndex);
    rootParam.put("endIndex", endIndex);
    quickSortStack.push(rootParam);

    // 循环结束条件:栈为空时
    while (!quickSortStack.isEmpty()) {
        //  栈顶元素出栈,得到起止下标
        Map<String, Integer> param = quickSortStack.pop();
        //  得到基准元素位置
        int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
        //  根据基准元素分成两部分, 把每一部分的起止下标入栈
        if (param.get("startIndex") < pivotIndex - 1) {
            Map<String, Integer> leftParam = new HashMap<>();
            leftParam.put("startIndex", param.get("startIndex"));
            leftParam.put("endIndex", pivotIndex - 1);
            quickSortStack.push(leftParam);
        }
        if (pivotIndex + 1 < param.get("endIndex")) {
            Map<String, Integer> rightParam = new HashMap<>();
            rightParam.put("startIndex", pivotIndex + 1);
            rightParam.put("endIndex", param.get("endIndex"));
            quickSortStack.push(rightParam);
        }
    }
}

Q&A

Q: 注意第22、23行,为什么要先让back - - ?

  • 因为此题的设定是从小到大(从左到右)排序,在最后的时候(即front==back),必须让他们所指的元素小于pivot元素(因为每次partition的最后还有一次swap),那么就必须先让back先–,因为back寻找的是就是小于pivot的元素,如果先让front++,它在最后时刻找到的就是大于pivot的元素,这就可能error了

Q: 那么如果是从大到小(从左到右)排序呢?(以第一个元素为pivot)

  • 这个时候,显然back这时候就该寻找最大的了,所以还是该让back先ki走,到最后时刻才不会找到最小的。这种情况只需修改22、23行里的判断条件就可以了。

Q: 当然,那么还有一个问题,如果以最后一个元素为pivot呢?

  • 答案是:这时候就让front先走

总结: 所以只存在两种情况:取决于pivot的位置。

  • 如果在最前,就让back先走;在最后,就让front先走(无论从小到大还是从大到小)
  • 一般取pivot在前,然后排序顺序不同的话只需要修改22、23行就行了

参考

  • 《漫画算法》by 程序员小灰

计数排序(CountingSort)

简介

  • 基本思路:假设输入元素序列的最大值和最小值差值为k,则创建一个长度为 k+1 的数组 count[],它的 count[i] 的值对应输入数组中 i 出现的次数。通过遍历一次输入数组并统计每个元素出现次数,最后遍历 count[] 输出。
  • 计数排序是一个基于非比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围)快于任何比较排序算法
  • 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))*的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)

算法实现

Java

public class CountingSort {
    public static void main(String[] args) {
        int[] array = new int[]{95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
        int[] sortedArray = countSort(array);
        System.out.println(Arrays.toString(sortedArray));
    }

    @SuppressWarnings("ForLoopReplaceableByForEach")
    public static int[] countSort(int[] array) {
        //1.得到数列的最大值和最小值,并算出差值d
        int max = array[0];
        int min = array[0];//最小值作为一个偏移量,用于计算整数在统计数组中的下标
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) max = array[i];
            if (array[i] < min) min = array[i];
        }
        int d = max - min;
        //2.创建统计数组并统计对应元素的个数
        int[] countArray = new int[d + 1];
        for (int i = 0; i < array.length; i++)
            countArray[array[i] - min]++;

        //3.统计数组做变形,后面的元素等于前面的元素之和
        for (int i = 1; i < countArray.length; i++)
            countArray[i] += countArray[i - 1];//让统计数组存储的元素值,等于相应整数的最终排序位置的序号

        //4.倒序(正序也可)遍历原始数列,从统计数组找到正确位置,输出到结果数组
        int[] sortedArray = new int[array.length];
        for (int i = array.length - 1; i >= 0; i--) {
            sortedArray[countArray[array[i] - min] - 1] = array[i];//这里减一是因为数组下标是从0开始的
            countArray[array[i] - min]--;
        }
        return sortedArray;
    }
}

算法分析

  • 假设原始数列的规模是n,最大和最小整数的差值是m
  • 代码第1、2、4步都涉及遍历原始数列,运算量都是n,第3步遍历统计数列,运算量是m,所以总体运算量是3n+m,去掉系数,时间复杂度是O(n+m)。
  • 空间复杂度:如果不考虑结果数组,只考虑统计数组大小的话,空间复杂度是O(m)。

特性

  • 优化版本的计数排序属于稳定排序

缺点

  1. 当数列最大和最小值差距过大时,并不适合用计数排序。例如给出20个随机整数,范围在0到1亿之间,这时如果使用计数排序,需要创建长度为1亿的数组。不但严重浪费空间,而且时间复杂度也会随之升高。
  2. 当数列元素不是整数时,也不适合用计数排序

对于这些局限性,另一种线性时间排序算法做出了弥补,这种排序算法叫作桶排序

桶排序

public class BucketSort {
    public static void main(String[] args) {
        double[] array = new double[]{4.12,6.421,0.0023,3.0,2.123,8.122,4.12, 10.09};
        double[] sortedArray = bucketSort(array);
        System.out.println(Arrays.toString(sortedArray));
    }

    public static double[] bucketSort(double[] array) {
        double max = array[0], min = array[0];
        for (int i = 1; i < array.length; ++i) {
            if (array[i] > max) max = array[i];
            if (array[i] < min) min = array[i];
        }
        double d = max - min;
        int bucketNum = array.length;
        ArrayList<LinkedList<Double>> bucketList = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; ++i)
            bucketList.add(new LinkedList<>());
        for (int i = 0; i < array.length; ++i) {
            int num = (int)((array[i] - min) * (bucketNum - 1)/d);//???
            bucketList.get(num).add(array[i]);
        }

        for (int i = 0; i < bucketList.size(); ++i){
            Collections.sort(bucketList.get(i));
        }
        double[] sortedArray = new double[array.length];
        int index = 0;
        for (LinkedList<Double> list : bucketList){
            for (double element : list){
                sortedArray[index++] = element;
            }
        }
        return sortedArray;
    }
}

堆排序

  • 堆排序是基于二叉堆的排序算法,二叉堆的节点“下沉”调整(downAdjust 方法)是堆排序算法的基础

  • 步骤如下

    1. 先把无序数组构建成二叉堆。若需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆
    2. 循环将堆顶元素替换到二叉堆的末尾,调整堆,产生新的堆顶
  • 堆排序是不稳定排序

复杂度分析

  • 第1步的时间复杂度是O(n)
  • 第2步需要进行n-1次循环。每次循环调用一次downAdjust方法,所以第2步的计算规模是 (n-1)×logn ,时间复杂度为O(nlogn)
  • 两个步骤是并列关系,所以整体的时间复杂度是O(nlogn)
  • 最坏时间复杂度也稳定在O(nlogn)
  • 空间复杂度是O(1)

Java实现

/**
 * 堆排序(降序,利用最小堆)
 */
public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 堆排序(降序,利用最小堆)
     * @param array 输入数组
     */
    public static void heapSort(int[] array) {
        // 1. 把无序数组构建成最小堆
        for (int i = (array.length / 2) - 1; i >= 0; i--)
            downAdjust(array, i, array.length);
        // 2. 循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
        for (int i = array.length - 1; i > 0; i--) {
            // 最后1个元素和第1个元素进行交换
            int temp = array[i];
            array[i] = array[0];
            array[0] = temp;
            // “下沉”调整最大堆
            downAdjust(array, 0, i);//注意这里长度为i,是本轮排序的有效范围
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值