排序方法的学习笔记

如何分析一个排序算法?

  • 排序算法的执行效率:最好情况、最坏情况、平均情况。时间复杂度的系数、常数、低阶。比较次数和交换次数
  • 排序算法的内存消耗:原地排序算法是指空间复杂度为O(1)的排序算法。
  • 排序算法的稳定性:待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的相对顺序不变。

一、冒泡排序

  • 定义:冒泡排序只会操作相邻的两个数据,每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小的关系要求。如果不满足就让他们俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序任务。
  • 冒泡排序的优化策略:当某次冒泡操作没有数据交换的时候,说明已经达到完全有序。(因为每次都是向上冒泡,没有数据交换说明前一个元素总是小于后一个元素)

代码实现

// 冒泡排序,a 表示数组,n 表示数组大小
public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 提前退出冒泡循环的标志位
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { // 交换
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;  // 表示有数据交换      
      }
    }
    if (!flag) break;  // 没有数据交换,提前退出
  }
}

1.为什么冒泡排序和插入排序的时间复杂度都是O(n^2),都是原地排序算法,为什么插入排序要比冒泡排序受欢迎呢?

  • 冒泡排序不管怎么优化,元素的交换次数是一个固定值,是初始数据的逆序度。插入排序是同样的,元素移动的次数也等于原始数据的逆序度。然而冒泡排序的数据交换要比插入排序的数据移动复杂。
    冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}

插入排序中数据的移动操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 数据移动
} else {
  break;
}

冒泡排序的性能分析?

  • 第一,冒泡排序是原地排序算法,冒泡的过程只涉及相邻数据的交换操作。
  • 第二,冒泡排序是稳定的排序算法。只有交换才改变两个元素的前后位置。为了保证稳定性,相邻的两个元素大小相等的时候,我们不做交换。相同大小的数据在排序前后不会改变顺序。
  • 冒泡最好的时间复杂度是o(n)最坏的情况是数据是倒序排列的,需要进行n次冒泡操作,最坏情况是o(n2).
  • 冒泡的逆序度:因为冒泡排序是基于比较和交换的,所以无论算法怎么改进,交换的次数是确定的。就是数列的逆序度。所以由交换次数可推出平均算法复杂度为o(n2).

二、插入排序

  • 定义:将数组中的数据分成两个空间,已排序空间和未排序空间。初始排序空间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间的数据一直有序,直到未排序中的元素为空。
// 插入排序,a 表示数组,n 表示数组大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;

  for (int i = 1; i < n; ++i) {
    int value = a[i];
    int j = i - 1;
    // 查找插入的位置
    for (; j >= 0; --j) {
      if (a[j] > value) {
        a[j+1] = a[j];  // 数据移动
      } else {
        break;
      }
    }
    a[j+1] = value; // 插入数据
  }
}

插入排序的性能分析?

  • 首先,插入排序是原地排序算法
  • 其次。插入排序是稳定的排序算法
  • 最后,插入排序最好的时间复杂度为o(n)。最坏的时间复杂度为o(n2)。

三、选择排序

  • 定义:分为已排序区间和未排序区间。选择排序每次会从未排序区间中找到最小的元素,将其放在已排序区间的末尾。这种排序方法是不稳定的。

四、归并排序

  • 归并排序使用的是分治的思想,将一个大问题分解成小的子问题来解决。小问题解决了,大问题也就解决了。
递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

终止条件:
p >= r 不用再继续分解

  • 上面的过程是将有序的两个数组合并成一个有序的数组。这时候我们要申请一个临时数组tmp。大小与A[p…r]相同。我们用连个游标i和j将两个有序数组的数字整合到数组tmp中。结束后,再把临时数组tmp中的数据拷贝到原数组A[p…r]中。代码如下:
public class Main {
 
	public static void main(String[] args) {
		int[] arr = {11,44,23,67,88,65,34,48,9,12};
		int[] tmp = new int[arr.length];    //新建一个临时数组存放
		mergeSort(arr,0,arr.length-1,tmp);
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" ");
		}
	}
	
	public static void merge(int[] arr,int low,int mid,int high,int[] tmp){
		int i = 0;
		int j = low,k = mid+1;  //左边序列和右边序列起始索引
		while(j <= mid && k <= high){
			if(arr[j] < =arr[k]){
				tmp[i++] = arr[j++];
			}else{
				tmp[i++] = arr[k++];
			}
		}
		//若左边序列还有剩余,则将其全部拷贝进tmp[]中
		while(j <= mid){    
			tmp[i++] = arr[j++];
		}
		
		while(k <= high){
			tmp[i++] = arr[k++];
		}
		
		for(int t=0;t<i;t++){
			arr[low+t] = tmp[t];
		}
	}
 
	public static void mergeSort(int[] arr,int low,int high,int[] tmp){
		if(low<high){
			int mid = (low+high)/2;
			mergeSort(arr,low,mid,tmp); //对左边序列进行归并排序
			mergeSort(arr,mid+1,high,tmp);  //对右边序列进行归并排序
			merge(arr,low,mid,high,tmp);    //合并两个有序序列
		}
	}
	
}

1.归并排序的性能分析?

  • 首先,归并排序稳不稳定关键看merge函数,即将两个子数组合并成一个有序数组的代码。在合并的过程中,我们可以先把前半段的元素放入到tmp数组中。这样就可以保证合并之后的先后顺序不变。

arr[j] < =arr[k]稳定
arr[j] < arr[k] 不稳定

  • 其次,归并排序的时间复杂度为o(nlogn)
T(a) = T(b) + T(c) + K  

T(1) = C;   n=1 时,只需要常量级的执行时间,所以表示为 C。
T(n) = 2*T(n/2) + n; n>1  

T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

当 T(n/2^k)=T(1) 时,也就是 n/2^k=1,k=log2n
T(n)=Cn+nlog2
  • 最后,归并排序不是原地排序算法。空间复杂度为o(n)因为在任意时刻cpu只有一个函数在执行,会申请临时内存空间,用完就释放掉了。

五、快速排序

递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

终止条件:
p >= r

  • 快速排序partion部分的代码

    index = a[i];
    while(i<j)
    {
        while(i<j&&a[j]>index)
            j--;
        if(i<j)
            a[i++]=a[j];
        while(i<j&&a[i]<index)
           i++;
        if(i<j)
            a[j--]=a[i];
    }
    a[i]=index;
    
  • 第二种实现类似于选择排序

    partition(A, p, r) {
    pivot := A[r]
    i := p
    for j := p to r-1 do {
     if A[j] < pivot {
       swap A[i] with A[j]
       i := i+1
     }
    }
    swap A[i] with A[r]
    return i
    

1.快排的性能分析

  • 快速排序是稳定,原地的排序算法。
  • 时间复杂度为o(nlogn)

2.快速排序与归并排序?

  • 快速排序是不稳定的,归并排序是稳定的。归并排序的处理过程是由下到上的,先处理子问题再合并。而快排刚好想法,由上到下,先分区再处理子问题。归并排序虽然是稳定的,但是不是原地排序算法。快速排序通过巧妙的设计解决了归并排序占用太多内存的问题。

如何用快排思想在O(n)内查找第K大元素?

我们选择数组区间 A[0…n-1] 的最后一个元素 A[n-1]作为pivot,对数组A[0,n-1]进行原地分区。分成三部分.

  • 当p+1=k,则找到了求解的元素
  • 当p+1<k,则在右边区间继续查找
  • 当p+1>k,则在左边区间继续查找

如果我们把每次分区遍历的元素个数加起来,就是n+0.5n+0.25n一直加到1,求和之后为2n-1,所以上面思路的时间复杂度就是O(n)。

如何优化快速排序算法?

  • 三数取中法:我们从区间的首、中、尾分别取一个数然后对比大小,去这三个数字的中间值作为分区点。
  • 随机法
    *c语言中的qsort。对于小数据量的排序采用归并排序,当数据量增大的时候哦采用快速排序。在递归的过程中当排序的区间中元素个数小于等于4的时候,qsort就退化成插入排序。

timsort

六、线性排序

  • 定义:时间复杂度是o(n)的排序算法,桶排序,计数排序和基数排序。这三种算法都是非基于比较的排序算法,不涉及元素之间的比较操作。

1.桶排序

  • 思想:将想要排序的数据分到几个有序的桶中,每个桶里的数据再进行单独排序。桶内排完序之后,再把每个桶里的数据按照次序取出,组内的序列就是有序的了。局限性:

1.首先桶和桶之间要有天然的大小书序,每个桶内的数据排序之后,桶间的数据不用再进行排序。
2.数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少。极端情况下,所有数据都在一个桶里,那就退化成o(nlogn)的算法。

  • 适用场景:适合外部排序中,即数据存储在外部磁盘中,数据量比较大,内存有限。
  • 实际场景问题:10GB的订单数据,我们希望按照订单金额进行排序,但是内存有限,只有几百兆,如何处理?

解:找到订单金额的最大最小值,将所有订单根据金额分到100个桶中。第一个桶存储的金额在1-1000之间的订单,依次类推。理想情况下,订单会被均匀分到100个文件中。每个文件中存储大约100mb的数据,然后我们可将将这100个文件依次放到内存中,用快排排序。最后按照文件编号,从小到大读取每个小文件中的订单数据。

2.基数排序

  • 比较手机号码的大小,如果前面几位中,a已经比b打了,那后面几位就不用看了。借助稳定排序算法。先按照最后一位排序,然后按照倒数第二额为重新排序。依次类推,最后按照第一位重新排序。经过11次排序之后,手机号码就都有序了。其中对于每一位的排序可以通过桶排序和计数排序,它们的时间复杂度可以达到o(n).如果要排序的数据有k位,那需要进行k次桶排序或者计数排序。

猜你喜欢

转载自blog.csdn.net/qq_22152499/article/details/89070565