1.分类
-
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
-
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
-
2.算法复杂度
3.相关概念
-
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
-
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
-
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
-
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
4.各类排序
一、冒泡排序
简单,不总结
二、选择排序(Selection Sort)
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
for (int i = 0 ; i < arr.length - 1; i ++){
int min = arr[i];
int index = i;
for (int j = i + 1 ; j < arr.length ; j ++ ){
if ( arr[j] < min){
min = arr[j];
index = j;
}
}
if (index != i){
arr[index] = arr[i];
arr[i] = min;
}
}
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
三、插入排序(Insertion Sort)
-
从第一个元素开始,该元素可以认为已经被排序;
-
取出下一个元素,在已经排序的元素序列中从后向前扫描;
-
如果该元素(已排序)大于新元素,将该元素移到下一位置;
for (int i = 1 ; i < array.length ; i ++) {
int insertVal = array[i];
int insertIndex = i - 1;
while (insertIndex >= 0 && insertVal < array[insertIndex]) {
array[insertIndex + 1] = array[insertIndex];
insertIndex--;
}
if (insertIndex + 1 != i)
array[insertIndex + 1] = insertVal;
}
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
四、希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
-
选择一个增量序列
-
按增量序列个数k,对序列进行k 趟排序;
-
每趟排序,根据对应的增量,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
for (int gap = array.length/2; gap > 0; gap /= 2){
for (int i = gap; i < array.length ; i ++){
int j = i;
int temp = array[j];
while (j - gap>= 0 && array[j] < array[j - gap] ){
array[j] = array[j - gap];
j -= gap;
}
array[j] = temp;
}
}
五、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
-
把长度为n的输入序列分成两个长度为n/2的子序列;
-
对这两个子序列分别采用归并排序;
-
将两个排序好的子序列合并成一个最终的排序序列。
public static void merge(int[] array,int left,int mid,int right,int[] temp){
int l = left;
int r = mid + 1;
int t = 0;
while (l <= mid && r <= right){
if (array[l] <= array[r] ){
temp[t] = array[l];
t ++;
l ++;
}else {
temp[t] = array[r];
t ++;
r ++;
}
}
// 剩余的数
while (l <= mid){
temp[t] = array[l];
l ++;
t ++;
}
while (r <= right){
temp[t] = array[r];
r ++;
t ++;
}
t = 0;
int templeft = left;
while (templeft <= right){
array[templeft] = temp[t];
t ++;
templeft ++;
}
}
public static void sort(int[] array,int left,int right,int[] temp){
if (left < right){
int mid = (left + right) /2;
// 左边
sort(array,left,mid,temp);
// 右边
sort(array,mid+1,right,temp);
merge(array,left,mid,right,temp );
}
}
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
六、快速排序(Quick Sort)
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
-
从数列中挑出一个元素,称为 “基准”(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就尽可能处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
private static int partition(int[] array, int left, int right) {
int low = left;
int high = right;
int pivot = array[low];
while (low < high) {
while (array[high] >= pivot && low < high) {
high--;
}
if (low < high) {
array[low] = array[high];
low++;
}
while (array[low] <= pivot && low < high) {
low++;
}
if (low < high) {
array[high] = array[low];
high--;
}
}
array[low] = pivot;
return low;
}
private static void sort(int[] array, int left, int right) {
if (left < right) {
int index = partition(array, left, right);
sort(array, left, index - 1);
sort(array, index + 1, right);
}
}
// 重载
private static void sort(int array[]) {
int left = 0;
int right = array.length - 1;
sort(array,left,right);
}
七、基数排序(Radix Sort)
-
基数排序的排序思路是这样的:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小……
-
排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是用“桶”来排序的。
-
由于某位数(个位/十位….,不是一整个数)的大小范围为0-9,所以我们需要10个桶,然后把具有相同数值的数放进同一个桶里,之后再把桶里的数按照0号桶到9号桶的顺序取出来,这样一趟下来,按照某位数的排序就完成了
public static void sort(int[] arr) {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
int maxlength = (max+"").length();
int[][] bucket = new int[10][arr.length];
int[] bucketindex = new int[10];
for (int i = 0 ,n = 1; i < maxlength ; i ++,n*=10) {
for (int j = 0; j < arr.length; j++) {
int digit = arr[j] /n % 10;
bucket[digit][bucketindex[digit]] = arr[j];
bucketindex[digit]++;
}
int index = 0;
for (int k = 0; k < bucketindex.length; k++) {
if (bucketindex[k] != 0) {
for (int l = 0; l < bucketindex[k]; l++) {
arr[index] = bucket[k][l];
index++;
}
}
bucketindex[k] = 0;
}
}
}
空间换时间的方法,当数据过大时,造成堆内存溢出。
八、堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
-
将初始待排序关键字序列构建成大顶堆,此堆为初始的无序区;
-
将堆顶元素R[1]与最后一个元素R[n]交换。
-
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];
// 左子树下标 = i * 2 + 1
// k = k * 2 + 1 (有可能调整完后,后面的子树比temp大,所以也要继续下去)
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k+1<length && arr[k] < arr[k+1]){
k++;
}
if (arr[k]>temp){
arr[i] = arr[k];
i = k;
}else { // 子树都比它小 后面的都小
break;
}
}
arr[i] = temp;
}
public static void sort(int[] arr){
int temp;
// (length/2 - 1) 从第一个非叶子结点从下至上,从右至左调整结构
for (int i = arr.length/2 - 1; i >= 0;i --){
adjustHeap(arr,i,arr.length);
}
for (int j = arr.length-1;j>0;j--){
temp=arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr,0,j);
}
}
5.算法选择
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
-
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
-
当n较大,内存空间允许,且要求稳定性 =》归并排序
-
当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
-
基数排序 它是一种稳定的排序算法,但有一定的局限性
1、关键字可分解。 2、记录的关键字位数较少,如果密集更好 3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。4、空间开销大
-
从稳定性看:直接插入排序、冒泡排序和归并排序是稳定的;而希尔排序、直接选择排序、快速排序和堆排序是不稳定排序;
-
从待排序的记录数n的大小看,n较小时,宜采用简单排序;而 n较大时宜采用改进排序。
由于各种排序方法各有优缺点,所以在不同的情况下可选择不同的方法。考虑的因素有:待排序的记录个数n;记录本身的大小;记录的关键字值分布情况;对排序稳定性的要求;辅助存储空间的大小等。综上所述,可以从以下几个方面来选择排序方法:
-
当待排序记录数 n 较大时,若要求排序稳定,则采用归并排序;当待排序记录数 n 较大,关键字分布随机,而且不要求稳定时,可采用快速排序;
-
当待排序记录数 n 较大,关键字会出现正、逆序情形,可采用堆排序(或归并排序);
-
当待排序记录数n 较小,记录已接近有序或随机分布时,又要求排序稳定,可采用直接插人排序;
-
当待排序记录数 n 较小,且对稳定性不作要求时,可采用直接选择排序。