文章目录
十大排序算法
插入排序
算法描述
插入排序是一种简单直观的算法。算法主要思想就是通过构造有序数列,对于未排序的数,从后向前扫描,找到相应的位置插入。
实现步骤
算法实现步骤描述:
- 把待排序的数组分为已排序和未排序两部分,初始化的时候认为数组的第一个元素是已排序好的。
- 从第二个元素开始,在已排序好的数组中找到合适的位置插入元素。
- 重复第二个过程,直到最后一个元素排序完。
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int position = i;
int value = arr[i];
while ( position > 0 && arr[position-1] > value){
arr[position] = arr[position-1];
position--;
}
arr[position] = value;
}
}
稳定性
由于插入的时候并不需要交换,所以插入排序是稳定的
使用场景
插入排序的算法复杂度是O(n2),数据量比较大的时候不适用。但是数据量比较小的时候性能还是比较好的,最佳的情况就是有序数列比较大时。
归并排序
算法描述
归并排序是典型的分治法案例。先将一个数列拆分成n/2个小数列,先将子序列排序,再将已有序的子序列合并成一个有序序列。如果是将两个有序数列合并成一个有序数列,这种排序叫归并排序。
实现步骤
- 将序列每相邻的两个数字进行归并操作,形成ceil(n/2)个数列,对每个数列进行排序,排序后每个数列有两或一个元素。
- 若此时数列数不是1,将上述数列进行再次归并,形成ceil(n/4)个数列,每个数列包含4或者3个元素。
- 重复步骤2,知道数列数为1.
public static void mergeSort(int[] arr){
int[] temp = new int[arr.length];
internalMergeSort(arr, temp, 0, arr.length-1);
}
private static void internalMergeSort(int[] arr, int[] temp, int left, int right){
if (left < right){
int middle = (left + right)/2;
internalMergeSort(arr, temp, left, middle);
internalMergeSort(arr, temp, middle+1, right);
mergeSortedArray(arr, temp, left, middle,right);
}
}
/**
* 合并已经排序好的两个数列
* @param arr
* @param temp
* @param left
* @param middle
* @param right
*/
private static void mergeSortedArray(int[] arr, int[] temp, int left, int middle, int right) {
int i = left;
int j = middle + 1;
int k = 0;
while ( i <= middle && j <= right ){
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while ( i <= middle ){
temp[k++] = arr[i++];
}
while ( j <= right ){
temp[k++] = arr[j++];
}
for (int n = 0; n < k; ++n) {
arr[left+n] = temp[n];
}
}
稳定性
归并排序,在数据相等的时候,都是以同样的顺序拷贝到辅助数组上的,所以归并排序也是稳定的。
适用场景
归并排序的时间复杂度为nO(log n) 在数量量比较大的时候,表现也比较出色;但是空间复杂度为O(n),在数据量比较大时,不可接受。
快速排序
算法描述
快速排序是一个比较知名的排序算法,其时间复杂度和空间复杂度表现都比较优秀
算法实现步骤
- 选定一个随机数,当作比较基准pivot,一般实现选数组的首位。
- 重新排序数列,所有比基准小的值放到基准值左边,所有比基准值大的值放到基准值右边,这个过程叫做分区。分区完成后,基准值在中间。
- 对于基准值左右两边的数列,递归的重复步骤2.
分区算法
- 设置两个变量low,high。快排开始的时候,low=0,high=arr.length-1。
- 选定基准值pivot=arr[low]。
- 从数组high开始遍历,直到找到第一个比基准值小的值,并赋值给arr[low]. arr[low] = arr[high]
- 从数组low开始遍历,直到找到第一个比基准值大的值,并赋值给arr[high].arr[high] = arr[low].
- 重复步骤3和步骤4,直到low大于等于high。
public static void quickSort(int[] arr){
int low = 0 ;
int high = arr.length - 1;
qSort(arr, low, high);
}
private static void qSort(int[] arr, int low, int high){
if( low >= high ){
return;
}
int pivot = partition(arr, low, high);
qSort(arr, low, pivot-1);
qSort(arr, pivot + 1, high);
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[low];
while ( low < high ){
while ( low < high && arr[high] >= pivot ){
--high;
}
arr[low] = arr[high];
while ( low < high && arr[low] <= pivot ){
++low;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
稳定性
对于相等的数据,无法保证扫描的顺序,所以不稳定
适用场景
快速排序适用于大部分场景,尤其是数据量大的时候。
堆排序
算法分析
二叉数是每个节点最多只有两个子节点的树结构,通常称为左子树和右子树。
堆排序的用到的最大堆和最小堆就是二叉树里的完全二叉树的一种实现方式。
二叉堆分为两种:最大堆和最小堆
最大堆
- 最大堆的最大值在根节点
- 每个父节点都比它的孩子节点要大
最小堆
- 最小堆的最小值在根节点。
- 每个父节点都比它的孩子节点小。
堆的建立和维护?
- 如果堆顶的元素被移除,那么堆的性质就被破坏了,如何重新调整堆来保证它的性质?
这里我们可以把最后一个元素(A),移到堆顶,然后拿他跟两个子节点比较,如果它比两个子节点大,则重新恢复堆的性质; 如果它比子节点小,则与子节点交换位置。重复这个过程。此过程中,A元素不断下沉,直到到合适的位置。
插入一个新节点,则与上述过程相反,只不过是把新节点放到末尾,递归的与父节点比较并交换。
- 给一堆无序数组,如何构造一个最大堆?
可以假设数组第一个元素就是满足条件的最大堆,然后插入新节点。
已知节点i,如果获取它的父节点、左子节点以及右子节点的位置。
父节点:(i-1)/2
左子节点:2i+1
右子节点: 2i+2
实现步骤
public class ArrayHeap {
private int[] arr;
public ArrayHeap(int[] arr) {
this.arr = arr;
}
/**
* 获取左子节点下标
* @param i
* @return
*/
private int getParentIndex(int i){
return (i-1)/2;
}
/**
* 获取左子节点下标
* @param i
* @return
*/
private int getLeftChildIndex(int i){
return 2*i + 1;
}
/**
* 获取右子节点下标
* @param i
* @return
*/
private int getRightChildIndex(int i){
return 2*i + 2;
}
/**
* 交换数组中的两个元素
* @param i
* @param j
*/
private void swap( int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 调整堆保证堆的性质
* @param i
* @param len
*/
private void adjustHeap(int i, int len){
int left, right, j;
left = getLeftChildIndex(i);
while ( left <= len ){
right = left + 1;
j = left;
//左子节点和右子节点比较
if( j < len && arr[left] < arr[right] ){
j++;
}
//父节点跟子节点中较大的元素比较并交换
if( arr[i] < arr[j] ){
swap( i, j);
i = j;
left = getLeftChildIndex(i);
} else {
break;
}
}
}
public void sort(){
//初始化最大堆
int last = arr.length - 1;
for ( int i = getParentIndex(last); i >= 0; --i ){
adjustHeap(i, last);
}
//堆调整
while ( last >= 0 ){
swap( 0, last--);
adjustHeap(0, last);
}
}
稳定性
因为堆排序涉及到大量的筛选和移动,属于不稳定算法
适用场景
堆排序在建立堆和调整堆的开销比较大,数据量比较小时,不太适合。
希尔排序
算法描述
希尔排序是插入排序的一种优化后的排序算法
算法描述:希尔排序是把序列按照下标进行增量分组,对每组使用直接插入算法排序;随着增量越来越小,每组的元素也越来也多。当增量为一时,排序结束。
实现步骤
public static void shellSort(int[] arr){
for( int delta = arr.length/2; delta >= 1; delta = delta/2){
for (int i = delta; i < arr.length; i++) {
for (int j = i; j >= delta && arr[j] < arr[j-delta] ; j -= delta) {
int temp = arr[j];
arr[j] = arr[j-delta];
arr[j-delta] = temp;
}
}
}
}
稳定性
因为涉及到多次增量分组的排序,所以稳定性不一定能保证,希尔排序时不稳定的。
适用场景
希尔排序实现简单,根据增量的不同,时间复杂度也有不同。 小数据量时适用,大数据量比不上快速排序。
-
第一种增量是最初Donald Shell提出的增量,即折半降低直到1。据研究,使用希尔增量,其时间复杂度还是O(n2)。
-
第二种增量Hibbard:{1, 3, …, 2k-1}。该增量序列的时间复杂度大约是O(n1.5)。
-
第三种增量Sedgewick增量:(1, 5, 19, 41, 109,…),其生成序列或者是94i - 92i + 1或者是4i - 3*2i + 1。
参考链接:https://zhuanlan.zhihu.com/p/42586566