目录
1.冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
1.1算法步骤
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.2效率最快
所有输入的数据都是正序
1.3效率最慢
所有输入数据是逆序时
1.4稳定性
所谓稳定的排序算法,指当原始数据中存在多个相同的值时,在排序后这些值的相对位置不变。
稳定性有什么用呢?
比如一个公司要统计所有部门的营业额,把这些部门按照营业额从小到大排序,当两个部门营业额相同时二者保持原来的顺序 ,对于这类问题就需要用稳定的排序算法来处理。
1.5复杂度
时间复杂度:O(n^2)
空间复杂度:O(1)
public class BubbleSort {
public static void BubbleSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-1-i; j++){
if(arr[j] > arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {4, 5, 3, 2, 1};
BubbleSort(arr);
for (int i : arr) {
System.out.print(i+" ");
}
}
}
2.选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法。
2.1简单选择排序
基本思想:每一趟从待排序的数元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
算法实现:每一趟通过不断地比较交换来使得首元素为当前最小,交换是一个比较耗时间的操作,我们可以通过设置一个值来记录较小元素的下标,循环结束后存储的就是当前最小元素的下标,这时再进行交换就可以了。
对于数组一个无序数组{4,6,8,5,9}来说,我们以min来记录较小元素的小标,i和j结合来遍历数组,初始的时候min和i都指向数组的首元素,j指向下一个元素,j开始从右向左进行遍历数组元素,若有元素比min元素更小则进行交换,然后min为更小元素下标,i再向右走,这样循环到i走到最后一个元素就完成了排序,过程如下图所示:
// 简单选择排序
public class SimpleSelectSort {
public void selectionSort(int[] arr){
// 需要遍历获得最小值的次数
// 需要注意,当排序N个数,已经经过 N-1 次遍历后吗,已经是有序数列
for (int i = 0; i < arr.length; i++){
int min = i; // 保存最小值的索引
// 寻找第i个小的数值
for (int j = i + 1; j < arr.length; j++){
if (arr[min] > arr[j]){
min = j;
}
// 若min有变化,就将找到的第i个小的数值与第i个位置上的数值交换
if (min != i){
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {3,4,5,2,1};
System.out.println("排序前:"+ Arrays.toString(arr));
SimpleSelectSort simpleSelectSort = new SimpleSelectSort();
simpleSelectSort.selectionSort(arr);
System.out.println("简单选择排序后:"+Arrays.toString(arr));
}
}
2.2 堆排序
堆是具有以下性质的完全二叉树:父节点的数据大于子节点的数据称之为大根堆,可以用来做升序;父节点的数据小于子节点的数据称之为小根堆,可以用来做降序。对于上面的数组{4,6,8,5,9}来说可以构成如下的树:
将它构成大根堆或者小根堆则如下图所示:
基本思想:将待排序序列构成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其于末尾元素进行交换,此时末尾就为最大值,然后将剩余n-1个元素重新构成一个堆,这样就会得到n个元素的次小值,如此反复执行就能得到一个有序序列。
算法实现:
步骤一:构造初始堆。将给定的无序序列构造成一个大顶堆。
(1)初始无序序列
(2)从最后一个非叶子节点开始,从左到右,从上到下进行调整。(第一个非叶子节点length/2 - 1 = 5/2 - 1 = 1,即下面的6节点)
(3)找到第二个非叶子节点4,因为{4,9,8}中9最大,则4和9交换
(4)这时的交换使得{4,5,6}的结构发生变化,继续调整,{4,5,6}中6最大,交换4和6
这样就将无序序列构造成了一个大根堆。
步骤二:将堆顶元素与数组的末尾元素交换,使得末尾元素最大,然后继续调整堆结构,再将堆顶元素与末尾元素交换,得到第二大元素,如此反复交换、调整就可以得到一个有序序列。
(1)将堆顶元素9与末尾元素4交换
(2)调整堆结构,让它继续满足堆定义
(3)将堆顶元素8与末尾元素5交换,得到第二大元素8
(4)将堆顶元素5与末尾元素4交换
(5)最后调整堆结构将5和6交换则得到了有序序列
public class HeapSort {
public static void heapSort(int []arr){
if (arr == null || arr.length < 2){
return ;
}
//假定用户给的数据是一个一个给的
//时间复杂度为O(nlogn) 自上而下
// for (int i=0;i<arr.length;i++){
// heapInsert(arr, i);
// }
//自下而上 时间复杂度为O(n)
for (int i=arr.length-1;i>=0;i--){
heapify(arr,i,arr.length);
}
int size = arr.length;
swap(arr,0,--size);
while(size>0){
heapify(arr,0,size);
swap(arr,0,--size);
}
}
private static void heapInsert(int []arr,int index){
while(arr[index]>arr[(index-1)/2]){
swap(arr,index,(index-1)/2);
index = (index-1)/2;
}
}
// 可以看做数据往下沉,
// 依次跟自己的值最大的子节点的值进行比较,
// 如果字节点的值比该节点的值大,就往下沉
private static void heapify(int []arr,int index,int size){
int left = 2*index+1;
while(left<size){//说明有孩子
int largestIndex = left+1<size && arr[left]<arr[left+1]?left+1:left;
largestIndex = arr[largestIndex]>arr[index]?largestIndex:index;
if (largestIndex == index){
break;
}
swap(arr,largestIndex,index);
index = largestIndex;
left = index*2+1;
}
}
private static void swap(int []arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int []arr = {5,4,3,2,1};
heapSort(arr);
for (int i : arr) {
System.out.print(i+" ");
}
}
}
3.插入排序
3.1直接插入排序
基本思想:每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
算法实现:直接插入排序是将无序序列中的数据插入到有序的序列中,在遍历无序序列时,首先拿无序序列中的首元素去与有序序列中的每一个元素比较并插入到合适的位置,一直到无序序列中的所有元素插完为止。对于一个无序序列arr{4,6,8,5,9}来说,我们首先先确定首元素4是有序的,然后在无序序列中向右遍历,6大于4则它插入到4的后面,再继续遍历到8,8大于6则插入到6的后面,这样继续直到得到有序序列{4,5,6,8,9}。
(1)我们用一个变量tmp存放关键字,因为我们先确定第一个元素是暂时有序的,所以tmp存放无序序列的第二个元素,然后i开始也为第二个元素的下标,j则为i-1,因为j要用有序的区域元素来与无序的区域元素比较。那么一开始i=1,tmp=6,j=0,因为6>4,所以6就不用进行插入;然后i向右走,i=2,tmp=arr[2]=8,j=i-1=1,8>6>4也不用插入。
(2)i继续向右走,i=3,tmp=arr[3]=5,j=i-1=2,5<8则要将8给5所在的元素数据,j向左走继续遍历有序区域。
(3)当j向右走到6时发现6>tmp=5,所以将6给它右边的第一个值(j+1的位置),再继续遍历有序区域,j=0时发现4<5则j+1的位置就是5该在的位置那么就将tmp的值5给j+1的位置的元素的值。
(4)再继续上面的操作,i最后到9发现比前面有序区域的元素都大,则不用再插入了,这样就得到了一个有序序列{4,5,6,8,9}。
public class Insertsort {
public static void insert(int[] arr){
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++){
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while(j > 0 && tmp < arr[j-1]){
arr[j] = arr[j-1];
j--;
}
// 存在比其小的数,插入
if (j != i){
arr[j] = tmp;
}
}
}
public static void main(String[] args) {
int[] arr = {3,5,4,2,1};
insert(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
}
3.2希尔排序
基本思想:希尔排序是把序列按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰好被分为一组,算法便终止。
初始时,有一个大小为 10 的无序序列。
(1)在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。
(2)接下来,按照直接插入排序的方法对每个组进行排序。
在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。
(3)按照直接插入排序的方法对每个组进行排序。
(4)在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。
(5)按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
时间复杂度:
最好情况:由于希尔排序的好坏和步长gap的选择有很多关系,因此,目前还没有得出最好的步长如何选择(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。
最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。
已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...)。
这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
空间复杂度
由直接插入排序算法可知,我们在排序过程中,需要一个临时变量存储要插入的值,所以空间复杂度为1。
算法稳定性
希尔排序中相等数据可能会交换位置,所以希尔排序是不稳定的算法。
public class ShellSort { // 希尔排序
public static void shellSort(int arr[]) {
int d = arr.length;//gap的值
while (true){
d = d/ 2;//每次都将gap的值减半
for (int x = 0; x< d; x++) {//对于gap所分的每一个组
for (int i = x+ d; i < arr.length; i= i + d) { //进行插入排序
int temp= arr[i];
int j;
for (j= i - d; j>= 0 && arr[j] > temp;j = j - d){
arr[j+ d] = arr[j];
}
arr[j+ d] = temp;
}
}
if (d== 1) {//gap==1,跳出循环
break;
}
}
}
public static void main(String[] args) {
int[] arr = {3,4,1,2,5};
shellSort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
}
4.归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
算法思路:
归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
- 将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
- 将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
假设我们有一个初始数列为{8, 4, 5, 7, 1, 3, 6, 2},整个归并排序的过程如下图所示。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
归并排序速度仅次于快速排序。时间复杂度是O(nlogn)。空间复杂度O(N),归并排序需要一个与原数组相同长度的数组做辅助来排序。归并排序是一种稳定的排序算法。
public class MergeSort {
public static void mergeSort(int[] arr){
if (arr == null || arr.length < 2){
return;
}
sort(arr,0,arr.length-1);
}
private static void sort(int[] arr, int left, int right){
if (left >= right){
return;
}
int mid = left + ((right-left) >> 1);
sort(arr, left, mid);
sort(arr, mid+1, right);
merge(arr, left, mid, right);
}
private static void merge(int[] arr, int start, int mid, int end){
int[] temp = new int[end-start+1];
int index = 0;
int left = start;
int right = mid+1;
while (left <= mid && right <= end){
temp[index++] = arr[left]>arr[right]?arr[right++]:arr[left++];
}
while (left <= mid){
temp[index++] = arr[left++];
}
while (right <= end){
temp[index++] = arr[right++];
}
for (int i = 0; i < temp.length; i++){
arr[start+i] = temp[i];
}
}
public static void main(String[] args) {
int[] arr = {5, 4, 3, 2, 1};
mergeSort(arr);
for (int i : arr) {
System.out.print(i+" ");
}
}
}
5.快速排序
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
排序算法的思想非常简单,在待排序的数列中,我们首先要找一个数字作为基准数(这只是个专用名词)。为了方便,我们一般选择第 1 个数字作为基准数(其实选择第几个并没有关系)。接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。
这是典型的分治思想,即分治法。下面我们对一个实际例子进行算法描述,讲解快速排序的排序步骤。
以 47、29、71、99、78、19、24、47 的待排序的数列为例进行排序,为了方便区分两个 47,我们对后面的 47 增加一个下画线,即待排序的数列为 47、29、71、99、78、19、24、47。
首先我们需要在数列中选择一个基准数,我们一般会选择中间的一个数或者头尾的数,这里直接选择第 1 个数 47 作为基准数,接着把比 47 小的数字移动到左边,把比 47 大的数字移动到右边,对于相等的数字不做移动。所以实际上我们需要找到中间的某个位置 k,这样 k 左边的值全部比 k 上的值小,k 右边的值全部比 k 上的值大。
接下来开始移动元素。怎么移动呢?其实冒泡排序也涉及对元素的移动,但是那样移动起来很累,比如把最后一个元素移动到第 1 个,就需要比较 n-1 次,同时交换 n-1 次,效率很低。其实,只需把第 1 个元素和最后一个元素交换就好了,这种思想是不是在排序时可以借鉴呢?之前说快速排序就是对冒泡排序的一个改进,就是这个原因。
快速排序的操作是这样的:首先从数列的右边开始往左边找,我们设这个下标为 i,也就是进行减减操作(i--),找到第 1 个比基准数小的值,让它与基准值交换;接着从左边开始往右边找,设这个下标为 j,然后执行加加操作(j++),找到第 1 个比基准数大的值,让它与基准值交换;然后继续寻找,直到 i 与 j 相遇时结束,最后基准值所在的位置即 k 的位置,也就是说 k 左边的值均比 k 上的值小,而 k 右边的值都比 k 上的值大。
所以对于上面的数列 47、29、71、99、78、19、24、47,进行第 1 趟第 1 个交换的排序情况如下,第 1 次的操作情况如图1所示。
交换之后,j 移动到了下标为 6 的位置,对 i 继续扫描,如图 2 所示。
此时交换后的数列变为 24、29、47、99、78、19、71、47。接下来我们继续对 i、j 进行操作,如图 3 所示,继续进行 i-- 及 j++ 的比较操作。
进行了这两次 i、j 的移动、比较、交换之后,我们最终得到的数列是 24、29、19、47、78、99、71、47。接下来我们继续进行 i-- 的操作,发现在 i 为 4 时比 47 大不用交换,在 i 为 3 时与 j 相遇,这时就不需要继续移动、比较了,已经找到 k 了,并且 k 的值为 3。我们可以确认一下当前的数列是不是 k 左边的值都比 47 小,而 k 右边的值都比 47 大(由于要保持相对位置不变,所以 47 同样在基准值 47 的右边)。
47 这个值已经落到了它该在的位置,第 1 趟排序完成了。接下来就是以 k 为基准,分为两部分,然后在左右两部分分别执行上述排序操作,最后数据会分为 4 部分;接着对每部分进行操作,直到每部分都只有一个值为止。
接下来进行第 2 趟排序,现在左边部分为 24、29、19,我们选择第 1 个数 24 作为基准数,接着进行 i--、j++ 的操作,我们发现 i 最初的值为 19,比 24 这个基准值小,所以与基准值进行交换,得到的数列为 19、29、24;当 j 为 1 时,我们发现 29 比 24 大,所以与基准值进行交换,得到的数列 19、24、29,此时 i 为 2,j 为 1;继续 i-- 时发现 i 为 1,与 j 相遇,左边部分的数列的 k 为 1,并且左右两部分分别只有一个元素,此时第 2 轮排序的左边部分的排序结束,同时左边部分的所有数据都排序完成。
我们接着看右边部分的排序,待排序的数列为 78、99、71、47,我们同样选择第 1 个值 78 为基准值,接下来进行 i 与 j 的移动与比较,发现 47 比 78 小,进行交换,得到的数列 47、99、71、78;从左往右发现 99 比基准值 78 大,进行交换,得到的数列为 47、78、71、99;继续从右向左看,发现 71 比基准值 78 小,进行交换,得到的数列为 47、71、78、99。此时 i 在整体数组中的下标为 6,j 为 5,若继续 j++ 则与 i 相遇,所以完成此轮排序。
此时右边数列的 k 为 6,一般会是相遇的位置,也就是基准值所在的位置,这时数列又被分为两部分,左边是 47、71,右边是 99,需要继续对左边部分的数据进行排序,虽然只有两个数据,但我们还是继续按照快速排序的思想操作一下,选择 47 作为基准数,将i进行从右向左的移动、比较,发现 i 与 j 相等时没有产生移动,完成第 2 轮排序。
至此,所有排序都已经完成,最终数列的结果是 19、24、29、47、47、71、78、99,怎么样,快速排序是不是非常简单地完成了所有的排序呢?虽然本次快速排序没有改变相同值的元素的顺序,但是由于快速排序需要对数列中的元素来回移动,有时还是会改变相对顺序的(比如 47 在第 1 轮的移动过程中就被移动到 47 的右边了),所以快速排序并不是一个稳定的算法。
/*
* 荷兰国旗问题
* 问题一:
* 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
* 要求额外空间复杂度O(1),时间复杂度O(N)
* 问题二:(荷兰国旗问题)
* 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
* 要求额外空间复杂度O(1),时间复杂度O(n)
* */
public class QuickSort {
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
// arr[l..r]排好序
public static void quickSort(int[] arr, int L, int R) {
if (L < R) {
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1);
quickSort(arr, p[1] + 1, R);
}
}
// 处理arr[l...r]的函数
// 默认以arr[r]做划分,arr[r] -> p <p ==p >p
// 返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0],res[1]
public static int[] partition1(int[] arr, int L, int R) {
int less = L - 1; // <区右边界
int more = R; // >区左边界
while (L < more) {
// L表示当前位置,arr[R] -> 划分值
if (arr[L] < arr[R]) { // 当前数 < 划分值
swap(arr, ++less, L++);
} else if (arr[L] > arr[R]) { //当前数 > 划分值
swap(arr, --more, L);
} else {
L++;
}
}
swap(arr, more, R);
return new int[]{less + 1, more};
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/*
* 1.0版本,时间复杂度O(n^2)
*
* */
public static void quickSort_1(int[] arr, int L, int R){
if (L >= R){
return;
}
int partition = arr[R];
int less = L - 1;
int more = R + 1;
int left = L;
while(left < more){
if(arr[left] > partition){
swap(arr, left, --more);
}else if (arr[left] == partition){
left++;
}else{
swap(arr, left++,++less);
}
}
quickSort_1(arr, L, less);
quickSort_1(arr, more, R);
}
/*
* 2.0版本:指定的基准值是随机的,时间复杂度最后收敛到O(nlogn)
*
* */
public static void quickSort_v2(int[] arr, int L, int R){
if (L < R){
// 随机选出一个值与最右边的元素交换
swap(arr, L+(int)(Math.random()*(R-L+1)),R);
// 将数组分成三部分,小于 等于 大于
// 返回值 0: 等于区域的第一个值 1:等于区域的最后一个值
int[] partition = partition(arr, L, R);
// 递归进行小于区域的分区
quickSort_v2(arr, L, partition[0]-1);
// 递归进行大于区域的分区
quickSort_v2(arr, partition[1]+1, R);
}
}
// 该方法将数组分成三部分,小于 等于 大于
// 返回两个参数表示,0;
private static int[] partition(int[] arr, int L ,int R){
int less = L-1;
int more = R;
while(L < more){
if (arr[L] < arr[more]){
swap(arr, L++, ++less);
} else if (arr[L] > arr[more]){
swap(arr, L, --more);
} else {
L++;
}
}
swap(arr, more, R);
return new int[]{less+1, more};
}
public static void main(String[] args) {
int[] arr= {1,4,3,7,5,3};
quickSort_v2(arr,0,arr.length-1);
for (int i : arr) {
System.out.print(i+" ");
}
}
}
快速排序是在冒泡排序的基础上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。
但是快速排序在最坏情况下的时间复杂度和冒泡排序一样,是 O(n2)
,实际上每次比较都需要交换,但是这种情况并不常见。我们可以思考一下如果每次比较都需要交换,那么数列的平均时间复杂度是 O(nlogn)
,事实上在大多数时候,排序的速度要快于这个平均时间复杂度。这种算法实际上是一种分治法思想,也就是分而治之,把问题分为一个个的小部分来分别解决,再把结果组合起来。
快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn)
,在最差的情况下,若每次只完成了一个元素,那么空间复杂度为 O(n)
。所以我们一般认为快速排序的空间复杂度为 O(logn)
。
快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。
快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
总结:
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O(N^2) | O(1) | × |
冒泡排序 | O(N^2) | O(1) | √ |
插入排序 | O(N^2) | O(1) | √ |
归并排序 | O(N*logN) | O(N) | √ |
快排 | O(N*logN) | O(logN) | × |
堆排 | O(N*logN) | O(1) | × |
计数排序 | O(N) | √ | |
基数排序 | O(N) | √ |
-
一般情况下选择一种排序算法的话,选择快速排序 (虽然快排和堆排时间复杂度是一样,但是快排的常数时间是最快的),只有空间有限制的时候才选择堆排序
-
基于比较的排序,没有一种算法可以将时间复杂度做到O(N*logN)以下
-
基于比较的排序,在时间复杂度为O(N*logN)情况下,空间复杂度在O(N)以下,没有一种算法可以做到稳定
-
常见的坑(非常难,不要求掌握,会破坏):
-
归并排序额外空间复杂度可以通过 “归并排序内部缓存法” 降为O(1),但是其稳定性就失去了
-
"原地归并排序"可以将额外空间复杂度变为O(1),但是时间复杂度会变成O(N^2)
-
快速排序可以通过论文"01 stable sort"将稳定性变为稳定,但是于此同时,其额外空间复杂度会变成O(N)
-
有一道题,是奇数的放在数组的左边,偶数放在数组的右边,还要求原来的相对次序不变,能实现,但是很难,可以参考论文"01 stable sort"!
经典的快排划分做不到稳定性,但是它是01标准,和奇偶问题一个调整策略,经典快排做不到,所以我不知道这个题怎么解
-
-
工程上对排序的排序
-
充分利用O(N*logN)和O(N^2)排序的各自优势
例如经典快排,在叫样本量的可以使用插入排序进行排序,大样本量在使用快排,利用了各自的优势
-
稳定性的考虑
基础类型的数据使用快排,非基础类型使用归并排序(保证稳定性)
-