十大经典排序算法思想及Java代码实现

978 阅读15分钟

排序算法稳定性:

  • 当碰到两个相同的值不进行交换,那么就说这个算法是稳定的。
  • 反之,这个算法就不稳定。

简单排序

冒泡排序

冒泡排序算法是稳定的排序算法。

冒泡排序思路

基本思路:冒泡排序,类似于水中冒泡,较大的数沉下去,较小的数慢慢冒起来,假设从小到大,即为较大的数慢慢往后排,较小的数慢慢往前排

直观表达:每一趟遍历,将一个最大的数移到序列末尾。

算法描述

  1. 第一趟排序:第1个和第2个元素比较,如果前一个比后一个大,就交换。接着第2个和第3个比较。直到倒数第2个和最后1个比较。此时,第一趟完成后,最大的元素就在序列的最后。
  2. 第二趟排序:第1个和第2个元素比较,直到倒数第三和倒数第二个。
  3. 不断重复,直到n-1趟。

下面为动图表示:

代码实现

package com.xgc.sort.simplesort.bubblesort;

/**
 * 冒泡排序
 * @author xgc
 *
 */
public class BubbleSort {
	
	public static void sort(int[] arr) {
		if (arr==null || arr.length<2) {
			return;
		}
		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;
				}
			}
		}
	}
}

测试

package com.xgc.sort.simplesort.bubblesort;

public class BubbleSortTest {
	
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		BubbleSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(n2)

空间复杂度: O(1)

稳定性:稳定

插入排序

插入排序思想

基本思想是将一个记录插入到已经排好序的有序表中,从而形成一个新的,元素个数+1的有序表。

下图为插入排序算法的排序过程表示:

代码实现

package com.xgc.sort.simplesort.insertionsort;

/**
 * 插入排序
 * @author 21952
 *
 */
public class InsertionSort {

	public static void sort(int[] arr) {
		for(int i=1; i<arr.length; i++) {
			//获取当前指向的元素
			int temp = arr[i];
			//记录在内层循环中当前指向的下标
			int j;
			//判断当前元素是否小于前一个元素,小于的话就开始比较并插入当前元素
			//从后往前比较
			for(j=i; j>0 && arr[j-1]>temp; j--) {
				//将元素往后移动
				arr[j] = arr[j-1];
			}
			//在内层循环结束之后,j指向的是temp元素要插入的位置
			arr[j] = temp;
		}
	}
}

测试

package com.xgc.sort.simplesort.insertionsort;

public class InsertionSortTest {
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		InsertionSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(n2)

空间复杂度: O(1)

稳定性:稳定

选择排序

选择排序思想

首先在未排序的序列中找到最小(大)元素,存放到排序序列的起始位置。然后在从剩余的未排序序列中找到最小(大)元素,放到排序序列的末尾。重复操作,直到未排序的元素为零。

代码实现

package com.xgc.sort.simplesort.selectionsort;

/**
 * 选择排序
 * @author xgc
 *
 */
public class SelectionSort {
	
	public static void sort(int[] arr) {
		for(int i=0; i<arr.length; i++) {
			//记录最小值的下标
			int min=i;
			for(int j=i+1; j<arr.length; j++) {
				if (arr[j] < arr[min]) {
					min = j;
				}
			}
			
			int temp = arr[i];
			arr[i] = arr[min];
			arr[min] = temp; 
		}
	}
}

测试

package com.xgc.sort.simplesort.selectionsort;

public class SelectionSortTest {
	
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		SelectionSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(n2)

空间复杂度: O(1)

稳定性:不稳定

希尔排序(Shell's Sort)

希尔排序是插入排序的一种,又叫缩小增量排序,是插入排序算法的一种更高效的改进版本。

希尔排序是非稳定算法。

希尔排序基于直接插入排序的以下两个特质而提出改进方法:

  1. 插入排序在几乎已经排好序的数据操作中,效率高,可以达到线性排序的效率。
  2. 插入排序一般是低效的,因为插入排序每次只能将数据移动一位。

希尔排序算法思想

先取一个小于n的整数d1作为第一个增量,把所有数据进行分组,所有距离为d1的数据放在同一个组中。先在组内进行直接插入排序。然后取第二个增量d2<d1,重复上面的分组和排序,直到所取的增量dt=1,即所有数据放在同一组进行插入排序为止。

该算法之所以能够提高效率的原因是:

相隔比较远的元素的一次交换能够跨过多个元素,即进行一次交换就可能消除多个逆序对。

代码实现

package com.xgc.sort.advancedsorting.shellsort;

/**
 * 希尔排序
 * @author xgc
 *
 */
public class ShellSort {
	
	public static void sort(int[] arr) {
		//计算增量step,这里我们也叫步长
		//增量初始为长度的一半,此外不断除2,直到step==1
		for(int step= arr.length/2; step>0; step/=2) {
			for(int i = step; i<arr.length; i++) {
				//记录要比较的数值
				int temp = arr[i];
				//记录内层循环当前指向的下标
				int j;
				for(j=i-step; j>=0 && arr[j]>temp; j-=step) {
					arr[j+step] = arr[j];
				}
				arr[j+step] = temp;
			}
		}
	}

}

测试

package com.xgc.sort.advancedsorting.shellsort;

public class ShellSortTest {
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		ShellSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:这个没有确定,根据增量序列的选择不同会有不同的时间复杂度。在上面的代码实现使用的是希尔增量序列。

空间复杂度: O(1)

稳定性:不稳定

堆排序(Heap Sort)

堆排序是指利用堆这种数据结构所设计的一种排序算法。

堆是一个近似完全二叉树的结构。

  • 最大堆:每个结点的值大于或等于其子结点的值,在堆排序算法中用于升序排序
  • 最小堆:每个结点的值小于或等于其子结点的值,在堆排序算法中用于降序排序

堆排序思想

  1. 将给定的无序序列构造成最大堆,此时最大的元素在堆首。
  2. 将堆首和堆尾交换,即将最大的元素沉到数组的末端
  3. 将堆的大小减1,再调整成最大堆。
  4. 不断重复至堆的大小为1

网上看堆排序,发现有篇文章的图解助于理解堆排序的过程,结合代码看,如下:

堆排序过程图解

代码实现

package com.xgc.sort.advancedsorting.heapsort;

/**
 * 堆排序(最大堆,升序排序)
 * @author xgc
 *
 */
public class HeapSort {
	
	public static void sort(int[] arr) {
		
		//用于记录堆长度
		int len = arr.length;
		
		//构建最大堆
		//从最后一个非叶结点开始从右往左,从下往上开始调整
		for(int i=arr.length/2-1; i>=0; i--) {
			heapify(arr, i, len);
		}
		
		//将堆首和堆尾交换,并将堆长度减一,调整剩下堆为最大堆
		for(int i = arr.length-1; i>0; i--) {
			swap(arr, 0, i);
			heapify(arr, 0, i);
		}
	}
	
	/**
	 * 调整成最大堆(仅在最大堆已建立的基础上)
	 * @param arr 要调整的数组
	 * @param i 保证i结点所在的树为最大堆
	 * @param len 要调整的堆的长度
	 */
	private static void heapify(int[] arr, int i, int len) {
		//最大值的下标
		int largest = i;
		//i节点的左子结点的下标
		int left = 2*i+1;
		//i节点的右子结点的下标
		int right = 2*i+2;
		
		if (left<len && arr[left]>arr[largest]) {
			largest = left;
		}
		
		if (right<len && arr[right]>arr[largest]) {
			largest = right;
		}
		
		if (largest != i) {
			swap(arr, i, largest);
			heapify(arr, largest, len);
		}
	}
	
	/**
	 * 将i结点和最大值的结点交换
	 * @param arr
	 * @param i
	 * @param largest
	 */
	private static void swap(int[] arr, int i, int largest) {
		int temp = arr[i];
		arr[i] = arr[largest];
		arr[largest] = temp;
	}
}

测试

package com.xgc.sort.advancedsorting.heapsort;

public class HeapSortTest {
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		HeapSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果如下:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(nlogn)

空间复杂度: O(1)

稳定性:不稳定

归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。

该算法是分治法(Divide and Conquer)的一个典型的应用。

归并排序思想

  1. 申请空间,使其大小为两个已排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,分别指向两个已排序序列的起始位置
  3. 比较两个指针指向的元素,选择小的元素放在合并空间,并移动指针到下一位置
  4. 重复步骤3,直到某一个指针超出序列尾,将另外一个序列的所有剩余元素直接赋值到合并序列尾

图解如下:图源)

代码实现

package com.xgc.sort.advancedsorting.mergesort;

public class MergeSort {
	
	public static void sort(int[] arr) {
		if (arr.length<2) {
			return;
		}
		
		//合并序列的存储空间
		int[] tmp = new int[arr.length];
		merge(arr, 0, arr.length-1, tmp);
	}
	
	/**
	 * 采用分治法来实现数组的排序
	 * @param arr 要排序的数组
	 * @param start 数组的起始下标
	 * @param end 数据的终点下标
	 */
	private static void merge(int[] arr, int start, int end, int[] tmp) {
		
		//判断传进来的start和end之间包含的元素个数是否为1
		//不是的话,继续分
		if (end - start > 0) {
			int middle = (start+end)/2;
			//对左半部分进行排序
			merge(arr, start, middle, tmp);
			//对右半部分进行排序
			merge(arr, middle+1, end, tmp);
			
			//左右部分排序完成后开始排序
			//记录左半部分的指针
			int left = start;
			//记录右半部分的指针
			int right = middle+1;
			//记录有序序列的下标
			int index = start;
			
			//将数组排序后两部分赋值到tmp数组对应的位置
			for(int i=start; i<=middle; i++) {
				tmp[i] = arr[i];
			}
			for(int i=middle+1; i<=end; i++) {
				tmp[i] = arr[i];
			}
			
			//开始比较
			while(left<=middle && right<=end) {
				if (tmp[left]<tmp[right]) {
					arr[index++] = tmp[left++];
				} else {
					arr[index++] = tmp[right++];
				}
			}
			
			//将剩余部分赋值
			while(left<=middle) {
				arr[index++] = tmp[left++];
			}
			while(right<=end) {
				arr[index++] = tmp[right++];
			}
		}
	}
}

测试

package com.xgc.sort.advancedsorting.mergesort;

public class MergeSortTest {
	
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		MergeSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(nlogn)

空间复杂度: O(n)

稳定性:稳定

快速排序(Quick Sort)

快速排序是对冒泡排序的一种改进

快速排序思想

  1. 从要排序的数据挑出一个元素,称为基准。
  2. 重新排序数列,比基准小的放在基准前面,比基准大的放在基准后面。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

代码实现

package com.xgc.sort.advancedsorting.quicksort;

public class QuickSort {
	
	public static void sort(int[] arr) {
		quicksort(arr, 0, arr.length-1);
	}

	/**
	 * 递归进行快速排序
	 * @param arr 要排序的数组
	 * @param left 要排序部分的开始下标
	 * @param right 要排序部分的结束下标
	 */
	private static void quicksort(int[] arr, int left, int right) {
		if(left<right) {
			int partitionIndex = partition(arr, left, right);
			quicksort(arr, left, partitionIndex-1);
			quicksort(arr, partitionIndex+1, right);
		}
	}

	/**
	 * 将大于基准值的数放在基准值右边,将小于基准值的数放在基准值左边
	 * @param arr 要排序的数组
	 * @param left 要排序部分的开始下标
	 * @param right 要排序部分的结束下标
	 * @return 基准数的下标
	 */
	private static int partition(int[] arr, int left, int right) {
		//记录基准值的下标
		int pivot = left;
		
		while(left<right) {
			while (arr[pivot] <= arr[right] && left<right) {
				right--;
			}
			while (arr[pivot] >= arr[left] && left<right) {
				left++;
			}
			
			if (left<right) {
				int tmp = arr[left];
				arr[left] =  arr[right];
				arr[right] = tmp;
			}
		}
		
		int tmp = arr[pivot];
		arr[pivot] = arr[left];
		arr[left] = tmp;
		return left;
	}
	
}

测试

package com.xgc.sort.advancedsorting.quicksort;

public class QuickSortTest {
	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		QuickSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}
}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(nlogn)

空间复杂度: O(1)

稳定性:不稳定

计数排序(Counting Sort)

计数排序是一种非基于比较的排序算法。它的优势在于对一定范围内的整数排序时,快于任何比较排序算法。这是一种牺牲空间换取时间的做法。

计数排序思想

对于一定范围内的整数,即在[m,n]范围整数的比较:

  1. 开辟一个新数组B,该数组长度为整数n-m+1
  2. 遍历数组,统计每个值为i的个数,存到数组B下标为i-m的位置
  3. 反向填充数组:将数组B中的值>0的数的下标i+m,赋值到目标数组,并将对应值减一

代码实现

package com.xgc.sort.advancedsorting.countingsort;

/**
 * 计数排序
 * @author xgc
 *
 */
public class CountingSort {
	
	public static void sort(int[] arr) {
		int[] values = getValue(arr);
		int minValue = values[0];
		int maxValue = values[1];
		int[] newArr = new int[maxValue-minValue+1];
		//遍历数组并计算同值的个数,赋值到新数组对应位置
		for(int value: arr) {
			newArr[value-minValue]++;
		}
		
		//反向填充目标数组
		int index = 0;
		for(int i=0; i<newArr.length; i++) {
			while (newArr[i] > 0) {
				arr[index++] = i+minValue;
				newArr[i]--;
			}
		}
	}

	/**
	 * 获取最小值和最大值的数组
	 * @param arr
	 * @return 数组长度为2,第一个为最小值,第二个为最大值
	 */
	private static int[] getValue(int[] arr) {
		int maxValue = arr[0];
		int minValue = arr[0];
		for(int value: arr) {
			if (value>maxValue) {
				maxValue = value;
			} else if (value < minValue) {
				minValue = value;
			}
		}
		int[] value = {minValue, maxValue};
		return value;
	}
	
}

测试

package com.xgc.sort.advancedsorting.countingsort;

public class CountingSortTest {

	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		CountingSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}

}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(n + k),k为整数的范围

空间复杂度: O(n+k)

稳定性:稳定

桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

桶排序思想

  1. 将待排序数据分到有限数量的桶子里。
  2. 每个桶子再分别排序(可以使用别的排序算法或递归使用桶排序进行排序)

代码实现

这里对每个桶使用了插入排序,具体代码见上面的 简单排序->插入排序 一节

package com.xgc.sort.advancedsorting.bucketsort;

import java.util.Arrays;

import com.xgc.sort.simplesort.insertionsort.InsertionSort;

public class BucketSort {
	
	/**
	 * 桶排序
	 * @param arr 要排序的数组
	 * @param bucketSize 每个桶的大小
	 */
	public static void sort(int[] arr, int bucketSize) {
		if (arr.length==0) {
			return;
		}
		
		int minValue = arr[0];
		int maxValue = arr[0];
		for (int value : arr) {
			if (value < minValue) {
				minValue = value;
			} else if(value > maxValue) {
				maxValue = value;
			}
		}
		
		int bucketCount = (int)Math.floor((maxValue-minValue) / bucketSize) + 1; 
		int[][] buckets = new int[bucketCount][0];
		
		//利用映射函数将数据分配到各个桶中
		//索引为i的桶存放的数据都比索引为i+1的桶大
		for (int i = 0; i < arr.length; i++) {
			int index = (int)Math.floor((arr[i]-minValue) / bucketSize);
			buckets[index] = arrAppend(buckets[index], arr[i]);
		}
		
		int arrIndex = 0;
		for(int[] bucket: buckets) {
			if (bucket.length<=0) {
				continue;
			}
			//对每个桶进行排序,这里使用插入排序
			InsertionSort.sort(bucket);
			for (int value : bucket) {
				arr[arrIndex++] = value;
			}
		}
	}

	/**
	 * 对桶自动扩容,并且保存数据
	 * @param arr 桶
	 * @param value 要保存的数组
	 * @return 扩容并保存了数据的桶
	 */
	private static int[] arrAppend(int[] arr, int value) {
		arr = Arrays.copyOf(arr, arr.length+1);
		arr[arr.length-1] = value;
		return arr;
	}
}

测试

package com.xgc.sort.advancedsorting.bucketsort;

public class BucketSortTest {

	public static void main(String[] args) {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		BucketSort.sort(arr, 3);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}

}

执行结果如下:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(n + c),c = N*(logN - logM),n为待排序数据个数,m为桶的个数

空间复杂度: O(n+m)

稳定性:稳定

基数排序(Radix Sort)

基数排序是一种非比较型整数排序算法。

基数排序思路

基数排序思路是将整数按位数切割成不同的数字,然后按每个位数分别比较

动图展示如下:

代码实现

package com.xgc.sort.advancedsorting.radixsort;

import java.util.Arrays;

/**
 * 基数排序(包括负数)
 * @author xgc
 *
 */
public class RadixSort {
	
	
	public static void sort(int[] arr) {
		int maxDigit = getMaxDigit(arr);
		
		int mod = 10;
		int dev = 1;
		
		for(int i=0; i<maxDigit; i++, mod*=10, dev*=10) {
			// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
			// 每一轮counter都会扩大10倍,第二轮[0,99]对应负数,[100,199]对应正数
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
            	//要是不考虑整数 bucket = arr[j]/dev
            	//此处的计算是考虑了负数的情况
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
		}
		
	}

	/**
	 * 获取数组中数据的最大位数
	 * @param arr
	 * @return 最大位数
	 */
	private static int getMaxDigit(int[] arr) {
		int maxValue = getMaxValue(arr);
		return getNumLength(maxValue);
	}

	/**
	 * 获取数组中的最大值
	 * @param arr 
	 * @return 最大值
	 */
	private static int getMaxValue(int[] arr) {
		int maxValue = arr[0];
		for (int value : arr) {
			if (value > maxValue) {
				maxValue = value;
			}
		}
		return maxValue;
	}
	
	/**
	 * 获取指定值的位数
	 * @param value 指定值
	 * @return 位数
	 */
	private static int getNumLength(int value) {
		if (value==0) {
			return 1;
		}
		
		int length = 0;
		for(int temp = value; temp!=0; temp/=10) {
			length++;
		}
		return length;
	}
	
	/**
	 * 扩容数组并保存数据
	 * @param arr 要扩容并保存数据的数组
	 * @param value 要保存的数据
	 * @return 扩容并保存数据后的数组
	 */
	private static int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
	}
}

测试

package com.xgc.sort.advancedsorting.radixsort;

public class RadixSortTest {

	public static void main(String[] args) throws Exception {
		int[] arr = {4,-1,5,9,16,10,7,4,-3};
		RadixSort.sort(arr);
		for (int i : arr) {
			System.out.print(i+",");
		}
	}

}

执行结果:

-3,-1,4,4,5,7,9,10,16,

复杂度

时间复杂度:O(d(r+n)),序列中最大值的位数为d,数字的基数为r,序列中元素个数为n

空间复杂度: O(rd+n)

稳定性:稳定