非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
描述
动画演示
代码实现
public static void main(String[] args) {
int[] array = {11, 33, 22, 44, 66, 77, 99, 55, 88};
int temp = 0;
for (int i = 0 ; i < array.length - 1 ; i++) {
for (int j = 0 ; j < array.length - 1 - i ; j++) {
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
动画演示
代码实现
public static void main(String[] args) {
int[] array = {11, 33, 22, 44, 66, 77, 99, 55, 88};
int temp = 0;
for (int i = 0 ; i < array.length - 1 ; i++) {
int minIndex = i;
for (int j = i + 1 ; j < array.length ; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
System.out.println(Arrays.toString(array));
}
特点
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
动画演示
代码实现
public static void main(String[] args) {
int[] array = {11, 33, 22, 44, 66, 77, 99, 55, 88};
int preIndex = 0;
int current = 0;
for (int i = 1 ; i < array.length ; i++) {
preIndex = i - 1;
current = array[i];
while (preIndex > 0 && array[preIndex] > current) {
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
System.out.println(Arrays.toString(array));
}
特点
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
希尔排序(shell sort)这个排序方法又称为缩小增量排序,是1959年D·L·Shell提出来的。该方法的基本思想是:设待排序元素序列有n个元素,首先取一个整数increment(小于n)作为间隔将全部元素分为increment个子序列,所有距离为increment的元素放在同一个子序列中,在每一个子序列中分别实行直接插入排序。然后缩小间隔increment,重复上述子序列划分和排序工作。直到最后取increment=1,将所有元素放在同一个子序列中排序为止。
描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
动画演示
代码实现
public static void main(String[] args) {
int[] arrays = {33, 11, 22, 44, 66, 77, 99, 55, 88, 87, 94, 25, 36, 45, 12, 37, 15, 14};
// 定义增量值
int gap = 0;
while (gap < arrays.length / 3) { // 动态定义间隔序列
gap = gap * 3 + 1;
}
for (int step = gap; step > 0; step /= 3) {
//从增量那组开始进行插入排序,直至完毕
for (int i = step; i < arrays.length; i++) {
int j = i;
int temp = arrays[j];
// j - step 就是代表与它同组隔壁的元素
// 循环
while (j - step >= 0 && arrays[j - step] > temp) {
arrays[j] = arrays[j - step];
j = j - step;
}
arrays[j] = temp;
}
}
System.out.println("最终结果:" + Arrays.toString(arrays));
}
特点
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
描述
动画演示
代码实现
public static void main(String[] args) {
int[] arrays = {33, 11, 22, 44, 66, 77, 99, 55, 88, 87, 94, 25, 36, 45, 12, 37, 15, 14};
merSort(arrays,0,arrays.length - 1);
System.out.println(Arrays.toString(arrays));
}
public static void merSort(int[] arr, int left, int right){
if(left < right){
int mid = (left + right) / 2;
//左边归并排序,使得左子序列有序
merSort(arr, left, mid);
//右边归并排序,使得右子序列有序
merSort(arr, mid+1, right);
//合并两个子序列
merge(arr, left, mid, right);
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];//ps:也可以从开始就申请一个与原数组大小相同的数组,因为重复new数组会频繁申请内存
int i = left;
int j = mid + 1;
int k = 0;
while(i <= mid && j <= right){
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[k++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[k++] = arr[j++];
}
//将temp中的元素全部拷贝到原数组中
for (int k2 = 0; k2 < temp.length; k2++) {
arr[k2 + left] = temp[k2];
}
}
特点
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
动画演示
代码实现
public static void main(String[] args) {
int[] arrays = {33, 11, 22, 44, 66, 77, 99, 55, 88, 87, 94, 25, 36, 45, 12, 37, 15, 14};
System.out.println("未排序数组是: " + Arrays.toString(arrays));
sort(arrays, 0, arrays.length - 1);
System.out.println("最终排序数组: " + Arrays.toString(arrays));
}
public static void sort(int[] arr, int low, int high){
int start = low;
int end = high;
// 获取第一个元素,用作“基准”
int key = arr[low];
while(end > start){
// 从后往前比较
while(end > start && arr[end] >= key) { // 如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
end--;
}
if(arr[end] <= key){
int temp = arr[end];
arr[end] = arr[start];
arr[start] = temp;
}
System.out.println("从后往前比较: " + Arrays.toString(arr));
// 从前往后比较
while(end > start && arr[start] <= key) { // 如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置
start++;
}
if(arr[start] >= key){
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
System.out.println("从前往后比较: " + Arrays.toString(arr));
// 此时第一次循环比较结束,关键值(基准)的位置已经确定了(基准已经处于排序正确的位置)。
// 左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用
}
// 递归--对左右序列进行排序
if(start > low)
sort(arr, low, start - 1);//左边序列。第一个索引位置到关键值索引-1
if(end < high)
sort(arr, end + 1, high);//右边序列。从关键值索引+1到最后一个
}
特点
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。
参考博客
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
描述
动画演示
分步讲解
代码实现
public static void main(String[] args) {
double [] A = {3, 7, 2, 11, 3, 4, 9, 2, 18, 0};
System.out.println("Input: " + Arrays.toString(A));
MaxHeap maxhp = new MaxHeap();
maxhp.heapsort(A);
System.out.println("Output: " + Arrays.toString(A));
}
// 获取父节点的index
protected static int parent(int i) {return (i - 1) / 2;}
// 获取左节点的index
protected static int left(int i) {return 2 * i + 1;}
// 获取右节点的index
protected static int right(int i) {return 2 * i + 2;}
static class MaxHeap {
protected double A[];
protected int heapsize;
//constructors
public MaxHeap(){}
public MaxHeap(double A[]){
buildMaxHeap(A);
}
/**
* <构造一个最大堆>
*
* @param A
* @return void
* @author Lifeifei
* @date 2018/12/27 10:14
*/
public void buildMaxHeap(double [] A){
this.A = A;
this.heapsize = A.length;
for (int i = parent(heapsize - 1); i >= 0; i--)
maxHeapify(i);
}
/**
* <保持最大堆特性>
*
* @param i
* @return void
* @author Lifeifei
* @date 2018/12/27 10:14
*/
protected void maxHeapify(int i){
int l = left(i);// 左节点(假设)
int r = right(i);// 右节点(假设)
int largest = i;// 根节点(假设)
// 判断 l 的值是否查过数组长度 && 判断左节点的值是否大于根节点
if (l <= heapsize - 1 && A[l] > A[i])
largest = l;
// 判断 r 的值是否查过数组长度 && 判断右节点的值是否大于根节点
if (r <= heapsize - 1 && A[r] > A[largest])
largest = r;
// 判断根节点是否等于假设值,如果不等于则将真实的最大值赋值给A[i]
if (largest != i) {
double temp = A[i];
// swap
A[i] = A[largest];
A[largest] = temp;
this.maxHeapify(largest);
}
}
/**
* <对数组进行“堆排序”>
*
* @param A
* @return void
* @author Lifeifei
* @date 2018/12/27 10:15
*/
public void heapsort(double [] A){
buildMaxHeap(A);
int step = 1;
for (int i = A.length - 1; i > 0; i--) {
double temp = A[i];
A[i] = A[0];
A[0] = temp;
heapsize--;
System.out.println("Step: " + (step++) + Arrays.toString(A));
maxHeapify(0);
}
}
}
public static void main(String[] args) {
int count = 10;
int[] A = new int[count];
Random random = new Random();
for (int i = 0 ; i < count ; i++) {
A[i] = random.nextInt(100);
}
System.out.println("Input: " + Arrays.toString(A));
System.out.println("start: " + System.currentTimeMillis());
sort(A);
System.out.println("Output: " + Arrays.toString(A));
System.out.println("end: " + System.currentTimeMillis());
}
public static void sort(int []arr){
// 1.构建大顶堆
// arr.length / 2 - 1 计算出的值到0之间都是非叶子节点
for(int i = arr.length / 2 - 1 ; i >= 0 ; i--){
// 从第一个非叶子结点从下至上,从右至左调整结构
// 第一次循环时,i 指的是第一个非叶子结点
adjustHeap(arr, i, arr.length);
}
// 2.调整堆结构 + 交换堆顶元素与末尾元素
System.out.println("bbbbb: " + Arrays.toString(arr));
for(int j = arr.length - 1 ; j > 0 ; j--){
// 将堆顶元素与末尾元素进行交换
swap(arr, 0, j);
// 重新对堆进行调整(自上至下)
adjustHeap(arr, 0, j);
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr, int i, int length){
// 先取出当前元素i
int temp = arr[i];
// 从i结点的左子结点开始,也就是2i+1处开始
for(int k = i * 2 + 1 ; k < length ; k = k * 2 + 1){
// 如果左子结点小于右子结点,k指向右子结点
if(k + 1 < length && arr[k] < arr[k+1]){
k++;
}
// 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
if(arr[k] > temp){
arr[i] = arr[k];
i = k;
}else{
break;
}
System.out.println("ccccc: " + Arrays.toString(arr));
}
arr[i] = temp;//将temp值放到最终的位置
System.out.println("aaaaa: " + Arrays.toString(arr));
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr, int a , int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
不稳定性
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个与其本身相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
参考博客
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。