Java - 八大排序算法及复杂度分析

Java - 八大排序算法及复杂度分析_第1张图片

内部排序:排序期间元素全部存放在内存中的排序

外部排序:排序期间元素无法全部存放在内存中,必须在排序过程中根据要求不断地进行内外存之间移动地排序

稳定性:指的是经过排序后,值相同的元素保持原来顺序中的相对位置不变

二分查找

public class BinarySearch {

	public static void main(String[] args) {
		int[] arr = {-3, 24, 69, 73, 80, 96, 101, 136};
		int index = binarySearch(arr, 101);
		System.out.println("index:" + index);
		
		int res = binarySearch(arr, 0, arr.length - 1, 101);
		System.out.println("res:" + res);
	}

	private static int binarySearch(int[] arr, int headIndex, int tailIndex, int num) {
		int midIndex = (headIndex + tailIndex) / 2;
		if (arr[midIndex] == num) {
			return midIndex;
		}else if (num > arr[midIndex]) {
			return binarySearch(arr, midIndex + 1, tailIndex, num);
		}else if (num < arr[midIndex]) {
			return binarySearch(arr, headIndex, midIndex - 1, num);
		}
		return -1;
	}

	private static int binarySearch(int[] arr, int num) {
		int headIndex = 0;
		int midIndex = arr.length / 2;
		int tailIndex = arr.length - 1;
		while (headIndex <= tailIndex) {
			if (arr[midIndex] == num) {
				return midIndex;
			}
			if (num > arr[midIndex]) {
				headIndex = midIndex + 1;
			}
			if (num < arr[midIndex]) {
				tailIndex = midIndex - 1;
			}
			midIndex = (headIndex + tailIndex) / 2;
		}
		return -1;
	}

}

(1)数组排序之冒泡排序

import java.util.Arrays;

public class BubbleSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, -9, 17};
		BubbleSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	private static void BubbleSort(int[] arr) {
		for (int i = 0; i < arr.length - 1; i++) {
			for (int j = 0; j < arr.length - 1 - i; j++) {
				if (arr[j] > arr[j + 1]) {
					int temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}	
		}
	}
}

复杂度分析

以下是冒泡排序算法复杂度:

冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²)。最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n)。平均来讲, 时间复杂度为O(n²)。由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1)。

总结与思考

由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。冒泡排序的特点:稳定,每次排序后,后面的元素肯定是已经排好序的,所以每次排序后可以确定一个元素在其最终的位置上,冒泡排序比较次数:n(n-1)/2,移动次数:3n(n-1)/2。


(2)数组排序之选择排序

import java.util.Arrays;

public class SelectSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, -9, 17};
		SelectSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	private static void SelectSort(int[] arr) {
		for (int i = 0; i < arr.length - 1; i++) {
			for (int j = i + 1; j < arr.length; j++) {
				if (arr[i] > arr[j]) {
					int temp = arr[i];
					arr[i] = arr[j];
					arr[j] = temp;
				}
			}
		}
		
	}

}

复杂度分析

以下是选择排序复杂度:

总结与思考

选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。

选择排序的特点:不稳定,每趟排序后前面的元素肯定是已经排好序的了,每次排序后可以确定一个元素会在其最终位置上。


(3)数组排序之直接插入排序

import java.util.Arrays;

public class InsertSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, -9, 17};
		insertSort(arr);
		System.out.println(Arrays.toString(arr));

	}

	private static void insertSort(int[] arr) {
		for (int i = 0; i < arr.length - 1; i++) {
			for (int j = i + 1; j > 0; j--) {
				if (arr[j - 1] > arr[j]) {
					int temp = arr[j - 1];
					arr[j - 1] = arr[j];
					arr[j] = temp;
				}
			}
		}
	}
}

注意:

如果 比较操作 的代价比 交换操作 大的话,可以采用二分查找法来减少 比较操作 的数目。该算法可以认为是 插入排序 的一个变种,称为二分查找插入排序

       插入排序又分为简单插入排序折半插入排序;简单插入排序思想是每趟排序把元素插入到已排好序的数组中,折半插入排序是改进的插入排序,由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。

复杂度分析

直接插入排序复杂度如下:

总结与思考

插入排序所需的时间取决于输入元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。


(4)数组排序之希尔排序

import java.util.Arrays;

public class ShellSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, -9, 17, 4};
		shellSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	private static void shellSort(int[] arr) {
		// 克努特序列
		int height = 1;
		while (height < arr.length / 3) {
			height = 3 * height + 1;
		}
		//for (int h = arr.length / 2; h > 0; h /= 2) {
		for (int h = height; h > 0; h = (h - 1) / 3) {
			for (int i = h; i < arr.length; i++) {
				for (int j = i; j > h - 1; j -= h) {
					if (arr[j] < arr[j - h]) {
						int temp = arr[j - h];
						arr[j - h] = arr[j];
						arr[j] = temp;
					}
				}
			}
		}
	}

}

复杂度分析

以下是希尔排序复杂度:

总结与思考

希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。

希尔排序不稳定,每次排序后不能保证有一个元素在最终的位置上。


(5)数组排序之快速排序

import java.util.Arrays;

public class QuickSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, -9, 17, 4};
		quickSort(arr, 0, arr.length - 1);
		System.out.println(Arrays.toString(arr));
	}

	private static void quickSort(int[] arr, int start, int end) {
		//找出分左右两区的索引位置,然后对左右两区进行递归调用
		if (start < end) {
			int index = getIndex(arr, start, end);
			quickSort(arr, start, index - 1);
			quickSort(arr, index + 1, end);
		}
		
	}

	/** 挖坑填数 **/
	private static int getIndex(int[] arr, int start, int end) {
		int i = start;
		int j = end;
		int num = arr[i];
		while (i < j) {
			//由后向前找比基准数小的数,找到后挖出此数填到前一个坑中
			while (i < j && arr[j] > num) {
				j--;
			}
			if (i < j) {
				arr[i] = arr[j];
				i++;
			}
			//由前向后找比基准数大的数,找到后挖出此数填到前一个坑中
			while (i < j && arr[i] < num) {
				i++;
			}
			if (i < j) {
				arr[j] = arr[i];
				j--;
			}
		}
		//把基准数填到最后一个坑中
		arr[i] = num;
		return i;
	}

	
}

复杂度分析

以下是快速排序算法复杂度:


(6)数组排序之归并排序

import java.util.Arrays;

public class MergeSort {

	public static void main(String[] args) {
		//int[] arr = {24, 69, 80, 57, 13, -9, 17, 4, 0};
		int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
		
		// 拆分
		splitOffArr(arr, 0, arr.length - 1);
		
		// 归并
		//mergeSort(arr, 0, arr.length / 2, arr.length - 1);
		System.out.println(Arrays.toString(arr));
	}

	// 拆分
	private static void splitOffArr(int[] arr, int startIndex, int endIndex) {
		// 计算中间索引位置
		int centerIndex = (startIndex + endIndex) / 2;
		if (startIndex < endIndex) {
			splitOffArr(arr, startIndex, centerIndex);
			splitOffArr(arr, centerIndex + 1, endIndex);
			// 归并
			mergeSort(arr, startIndex, centerIndex, endIndex);
		}
	}

	// 归并
	private static void mergeSort(int[] arr, int startIndex, int centerIndex, int endIndex) {
		// 定义一个临时数组
		int[] tempArr = new int[endIndex - startIndex + 1];
		// 定义左数组的起始索引
		int leftIndex = startIndex;
		// 定义右数组的起始索引
		int rightIndex = centerIndex + 1;
		// 定义临时数组的起始索引
		int index = 0;
		// 比较左右两个数组的元素大小,往临时数组中放
		while (leftIndex <= centerIndex && rightIndex <= endIndex) {
			if (arr[leftIndex] > arr[rightIndex]) {
				tempArr[index] = arr[rightIndex];
				rightIndex++;
			} else {
				tempArr[index] = arr[leftIndex];
				leftIndex++;
			}
			index++;
		}
		// 处理剩余元素
		while (leftIndex <= centerIndex) {
			tempArr[index] = arr[leftIndex];
			index++;
			leftIndex++;
		}
		while (rightIndex <= endIndex) {
			tempArr[index] = arr[rightIndex];
			index++;
			rightIndex++;
		}
		// 将临时数组中的元素取到原数组中
		for (int i = 0; i < tempArr.length; i++) {
			arr[i + startIndex] = tempArr[i];
		}
	}

}

复杂度分析

以下是归并排序算法复杂度:

从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。

总结与思考

归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需时间和NlogN成正比,它的主要缺点则是他所需的额外空间和N成正比。


(7)数组排序之基数排序

import java.util.Arrays;

public class RadixSort {
	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, 10, 17, 4, 0};
		RadixSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	/** 基数排序只能排正整数 **/
	private static void RadixSort(int[] arr) {
		int[][] tempArr = new int[10][arr.length];
		int max = getMax(arr);
		int len = String.valueOf(max).length();
		int[] record = new int[10]; 
		for (int i = 0,n = 1; i < len; i++,n *= 10) {
			for (int j = 0; j < arr.length; j++) {
				int num = arr[j] / n % 10;
				tempArr[num][record[num]] = arr[j];
				record[num] += 1;
			}
			int index = 0;
			for (int j = 0; j < tempArr.length; j++) {
				for (int k = 0; k < record[j]; k++) {
					arr[index] = tempArr[j][k];
					index++;
				}
				record[j] = 0;
			}
		}
	}
	private static int getMax(int[] arr) {
		int max = arr[0];
		for (int i = 1; i < arr.length; i++) {
			if (arr[i] > max) {
				max = arr[i];
			}
		}
		return max;
	}
}

复杂度分析

以下是基数排序算法复杂度,其中k为最大数的位数:

其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d*(n + r))。

总结与思考

基数排序更适合用于对时间, 字符串等这些 整体权值未知的数据 进行排序。

基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

基数排序:根据键值的每位数字来分配桶

计数排序:每个桶只存储单一键值

桶排序:每个桶存储一定范围的数值


(8)数组排序之堆排序

import java.util.Arrays;

public class HeapSort {

	public static void main(String[] args) {
		int[] arr = {24, 69, 80, 57, 13, 10, 17, 4, 0};
		// 从最后一个非叶子节点开始
		int index = (arr.length - 1) / 2;
		for (int i = index; i >= 0; i--) {
			heapSort(arr, arr.length, i);
		}
		
		for (int i = arr.length - 1; i > 0; i--) {
			int temp = arr[0];
			arr[0] = arr[i];
			arr[i] = temp;
			heapSort(arr, i, 0);
		}
		System.out.println(Arrays.toString(arr));
	}

	private static void heapSort(int[] arr, int size, int index) {
		int leftChildIndex = index * 2 + 1;
		int rightChildIndex = index * 2 + 2;
		int maxIndex = index;
		if (leftChildIndex < size && arr[leftChildIndex] > arr[maxIndex]) {
			maxIndex = leftChildIndex;
		}
		if (rightChildIndex < size && arr[rightChildIndex] > arr[maxIndex]) {
			maxIndex = rightChildIndex;
		}
		if (maxIndex != index) {
			int temp = arr[maxIndex];
			arr[maxIndex] = arr[index];
			arr[index] = temp;
			heapSort(arr, size, maxIndex);
		}
	}
}

复杂度分析

  1. 建立堆的过程, 从length/2 一直处理到0, 时间复杂度为O(n);
  2. 调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度, 时间复杂度为O(lgn);
  3. 堆排序的过程由n次第2步完成, 时间复杂度为O(nlgn)

Java - 八大排序算法及复杂度分析_第2张图片

总结与思考

由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。

 

你可能感兴趣的:(Java,#SortALGO)