当前位置 博文首页 > FMC_WBL的博客:Re:从零开始的DS学习 十大排序算法我都整理好了
Re:从零开始的DS学习 十大排序算法我都整理好了,详细介绍了冒泡、选择、插入、希尔、归并、快速、堆、计数、桶、基数、外部排序的算法流程和源码。供读者理解与学习,适合点赞+收藏。有什么错误希望大家直接指出~
数据结构和算法中,关于排序有十大算法,包括冒泡排序,简单选择排序,简单插入排序,归并排序,堆排序,快速排序、希尔排序、计数排序,基数排序,桶排序。
一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。对于其他排序可能会要求比较各自的优劣、各种算法的思想及其使用场景,还有要知道算法的时间和空间复杂度。
通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。我们将由易到难学习这十种算法。
想放一张总结图,最后会详细总结
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为相关的元素会经由交换慢慢“浮”到数列的顶端。
基本思路:
1、比较相邻的元素。如果第一个比第二个大(小),就交换它们两个;
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大(小)的数;
3、针对所有的元素重复以上的步骤,除了最后一个;重复步骤1~2,直到排序完成。
/**
* 类说明:冒泡算法(升序)
*/
public class BubbleSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
/*循环数组长度的次数*/
for (int i = 0; i < array.length; i++) {
/*从第0个元素开始,依次和后面的元素进行比较
* j < array.length - 1 - i表示第[array.length - 1 - i]
* 个元素已经冒泡到了合适的位置,无需进行比较,可以减少比较次数*/
for (int j = 0; j < array.length - 1 - i; j++) {
/*如果第j个元素比后面的第j+1元素大,交换两者的位置*/
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
return array;
}
}
选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
具体步骤:
首先,找到数组中最大(小)的那个元素;
其次,将它和数组的第一个元素交换位置(如果第一个元素就是最大(小)元素那么它就和自己交换);
再次,在剩下的元素中找到最大(小)的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。
/**
* 类说明:简单选择排序(升序)
*/
public class ChoiceSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex=i;/*最小数的下标,每个循环开始总是假设第一个数最小*/
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) /*找到最小的数*/
minIndex = j; /*将最小数的索引保存*/
}
/*交换最小数和i当前所指的元素*/
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
}
插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。
具体步骤:
1、对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
2、为了给要插入的元素腾出空间,我们需要将插入位置之后的已排序元素在都向右移动一位。
插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。
/**
* 类说明:简单插入排序(升序)
*/
public class InsertionSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
int currentValue;/*当前待排序数据,该元素之前的元素均已被排序过*/
for (int i = 0; i < array.length - 1; i++) {
int preIndex = i;/*已被排序数据的索引*/
currentValue = array[preIndex + 1];
System.out.println("待排序元素索引:"+(i + 1)+",值为:" +currentValue+
",已被排序数据的索引:"+preIndex);
/*在已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
将比较的元素元素后移一位*/
while (preIndex >= 0 && currentValue < array[preIndex]) {
//将当前元素后移一位
array[preIndex + 1] = array[preIndex];
preIndex--;
}
/*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
array[preIndex + 1] = currentValue;
}
return array;
}
}
一种基于插入排序的快速的排序算法(请大家先学习插入排序,了解基本的插入排序的思想。对于大规模乱序数组插入排序很慢,因为元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1 次移动。
希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是冲破O(n^2)的第一批算法之一。
希尔排序是把待排序数组按一定增量的分组,对每组使用直接插入排序算法排序;然后缩小增量继续分组排序,随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的增量,就构成了一个增量序列。
在先前较大的增量下每个子序列的规模都不大,用直接插入排序效率都较高,尽管在随后的增量递减分组中子序列越来越大,由于整个序列的有序性也越来越明显,则排序效率依然较高。
从理论上说,只要一个数组是递减的,并且最后一个值是1,都可以作为增量序列使用。有没有一个步长序列,使得排序过程中所需的比较和移动次数相对较少,并且无论待排序列记录数有多少,算法的时间复杂度都能渐近最佳呢?但是目前从数学上来说,无法证明某个序列是“最好的”。
常用的增量序列有:
希尔增量序列 :{N/2, (N / 2)/2, ..., 1},其中N为原始数组的长度,这是最常用的序列,但却不是最好的
Hibbard序列:{2^k-1, ..., 3,1}
Sedgewick序列:{... , 109 , 41 , 19 , 5,1} 表达式为
/**
* 类说明:希尔排序
*/
public class ShellSort {
public static int[] sort(int[] array) {
int len = array.length;
/*按增量分组后,每个分组中,temp代表当前待排序数据,该元素之前的元素均已被排序过*/
/*gap指用来分组的增量,会依次递减*/
int currentValue, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
currentValue = array[i];
/*组内已被排序数据的索引*/
int preIndex = i - gap;
/*在组内已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
并将比较的元素元素在组内后移一位*/
while (preIndex >= 0 && array[preIndex] > currentValue) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
/*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
array[preIndex + gap] = currentValue;
}
System.out.println("本轮增量【"+gap+"】排序后的数组");
gap /= 2;
}
return array;
}
}
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为2-路归并,与之对应的还有多路归并。
对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
为了提升性能,有时我们在半子表的个数小于某个数(比如15)的情况下,对半子表的排序采用其他排序算法,比如插入排序。
import java.util.Arrays;
/**
* 类说明:归并排序
*/
public class MergeSort {
public static int[] sort(int[] array) {
if (array.length < 2) return array;
/*切分数组,然后递归调用*/
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(sort(left), sort(right));
}
/**
* 归并排序——将两段排序好的数组结合成一个排序数组
*
* @param left
* @param right
* @return
*/
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)/*左边数组已经取完,完全取右边数组的值即可*/
result[index] = right[j++];
else if (j >= right.length)/*右边数组已经取完,完全取左边数组的值即可*/
result[index] = left[i++];
else if (left[i] > right[j])/*左边数组的元素值大于右边数组,取右边数组的值*/
result[index] = right[j++];
else/*右边数组的元素值大于左边数组,取左边数组的值*/
result[index] = left[i++];
}
System.out.print("左子数组:");
PrintArray.print(left);
System.out.print("右子数组:");
PrintArray.print(right);
System.out.print("合并后数组:");
PrintArray.print(result);
System.out.println("--------------------");
return result;
}
}
快速排序被誉为20 世纪科学和工程领域的十大算法之一。快速排序(Quicksort)是对冒泡排序的一种改进,也是采用分治法的一个典型的应用。
首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。在实际实现时,一般会在原数组上直接操作。
通过一趟快速排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
为了提升性能,有时我们在分割后独立的两部分的个数小于某个数(比如15)的情况下,会采用其他排序算法,比如插入排序。
快速排序中的基准数
基准的选取:最优的情况是基准值刚好取在无序区的中间,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数,但是一般很难做到最优。基准的选取一般有三种方式,选取数组的第一个元素,选取数组的最后一个元素,以及选取第一个、最后一个以及中间的元素的中位数(如4 5 6 7, 第一个4, 最后一个7, 中间的为5, 这三个数的中位数为5, 所以选择5作为基准)。
Dual-Pivot快排:两个基准数的快速排序算法,其实就是用两个基准数, 把整个数组分成三份来进行快速排序,在这种新的算法下面,比经典快排从实验来看节省了10%的时间。
/**
* 类说明:快速排序
*/
public class QuickSort {
public static int[] sort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end)
return null;
/*数据分割成独立的两部分时,从哪儿分区的指示器*/
int zoneIndex = partition(array, start, end);
if (zoneIndex > start)
sort(array, start, zoneIndex - 1);
if (zoneIndex < end)
sort(array, zoneIndex + 1, end);
System.out.println("本轮排序后的数组");
PrintArray.printIndex(array, start, end);
System.out.println("--------------------");
return array;
}
/**
* 快速排序算法——partition
*
* @param array
* @param start
* @param end
* @return
*/
public static int partition(int[] array, int start, int end) {
int pivot = (int) (start + Math.random() * (end - start + 1));
System.out.println("开始下标:" + start + ",结束下标:" + end + ",基准数下标:"
+ pivot + ",元素值:" + array[pivot]);
/*zoneIndex是分割指示器
从业务上来说:比基准数小的,放到指示器的左边,比基准数大的,放到指示器的右边,
* 但在实际实现时,通过移动比基准数小的元素和分割指示器本身也可以达到一样的效果*/
int zoneIndex = start - 1;
swap(array, pivot, end);/*将基准数和数组尾元素交换位置*/
for (int i = start; i <= end; i++) {
if (array[i] <= array[end]) {/*当前元素小于等于基准数*/
zoneIndex++;/*首先分割指示器累加*/
if (i > zoneIndex)/*当前元素在分割指示器的右边时,交换当前元素和分割指示器元素*/
swap(array, i, zoneIndex);
}
System.out.println("zoneIndex:" + zoneIndex + ",i:" + i);
PrintArray.printIndex(array, start, end);
}
System.out.println("---------------");
return zoneIndex;
}
/**
* 交换数组内两个元素
*
* @param array
* @param i
* @param j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,我们每次只需要操作数据中的最大元素(最小元素),那么有一种基于二叉堆的数据结构可以提供支持。
所谓二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点。
,这就是一个典型的二叉堆。
堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。
/**
* 类说明:堆排序
*/
public class HeapSort {
//声明全局变量,用于记录数组array的长度;
private static int len;
public static int[] sort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
buildMaxHeap(array);
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
PrintArray.print(array);
System.out.println("--------------------");
}
return array;
}
/**
* 建立最大堆
*
* @param array
*/
public static void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
for (int i = (len/2-1); i >= 0; i--) {
adjustHeap(array, i);
}
System.out.println("构造完成最大堆");
PrintArray.print(array);
System.out.println("============================================");
}
/**
* 调整使之成为最大堆
*
* @param array
* @param i
*/
public static void adjustHeap(int[] array, int i) {
int maxIndex = i;
int left = 2*i+1;
int right = 2*(i+1);
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (left < len && array[left] > array[maxIndex])
maxIndex = left;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (right < len && array[right] > array[maxIndex])
maxIndex = right;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
/**
* 交换数组内两个元素
* @param array
* @param i
* @param j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
计数排序是一个排序时不比较元素大小的排序算法。
计数排序对一定范围内的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序,而且待排序元素值分布较连续、跨度小的情况。
如果一个数组里所有元素都是整数,而且都在0-K以内。那对于数组里每个元素来说,如果能知道数组里有多少项小于或等于该元素,就能准确地给出该元素在排序后的数组的位置。
比如有个数组
对于这个数组来说,元素5之前有8个元素小于等于5(含5本身),因此排序后5所在的位置肯定是7.只要构造一个(5+1)大小的数组,里面存下所有对应A中每个元素之前的元素个数,就能在线性时间内完成排序。
import java.util.Arrays;
/**
* 类说明:CountingSort计数排序
*/
public class CountingSort {
public static int[] sort(int[] array) {
if (array.length == 0) return array;
/*寻找数组中最大值,最小值
* bias:偏移量,用以定位原始数组每个元素在计数数组中的下标位置*/
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
/*获得计数数组的容量*/
int[] counterArray = new int[max - min + 1];
Arrays.fill(counterArray, 0);
/*遍历整个原始数组,将原始数组中每个元素值转化为计数数组下标,
并将计数数组下标对应的元素值大小进行累加*/
for (int i = 0; i < array.length; i++) {
counterArray[array[i] + bias]++;
}
System.out.println("计数数组为:");
PrintArray.print(counterArray);
System.out.println("============================================");
int index = 0;/*访问原始数组时的下标计数器*/
int i = 0;/*访问计数数组时的下标计数器*/
/*访问计数数组,将计数数组中的元素转换后,重新写回原始数组*/
while (index < array.length) {
/*只要计数数组中当前下标元素的值不为0,就将计数数组中的元素转换后,重新写回原始数组*/
if (counterArray[i] != 0) {
array[index] = i - bias;
counterArray[i]--;
index++;
} else
i++;
PrintArray.print(counterArray);
PrintArray.print(array);
System.out.println("--------------------");
}
return array;
}
}
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。
基本步骤是:
1、根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;
2、将落在特定范围内的所有元素放入对应的桶中;
3、对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;
4、按照划分的范围顺序,将桶中的元素依次取出。排序完成。?
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
import java.util.ArrayList;
/**
* 类说明:桶排序
*/
public class BucketSort {
/**
*
* @param array
* @param bucketSize BucketSize,作为每个桶所能放置多少个不同数值
* (例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,
* 但是容量不限,即可以存放100个3);
* @return
*/
public static ArrayList<Integer> sort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
/*获得桶的数量*/
int bucketCount = (max - min) / bucketSize + 1;
/*构建桶*/
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
/*将原始数组中的数据分配到桶中*/
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
/*看看桶中数据的分布*/
for (int i = 0; i < bucketArr.size(); i++) {
System.out.print("第"+i+"个桶包含数据:");
PrintArray.printObject(bucketArr.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) {
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
/*对桶中的数据再次用桶进行排序*/
ArrayList<Integer> temp = sort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
}
常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位0~9数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,同时每一轮排序都基于上轮排序后的结果;当我们将所有的位排序后,整个数组就达到有序状态。比如对于数字2985,从右往左就是先以个位为关键字进行排序,然后是十位、百位、千位,总共需要四轮。基数排序不是基于比较的算法。
基数是什么意思?对于十进制整数,每一位都只可能是0~9中的某一个,总共10种可能。那10就是它的基,同理二进制数字的基为2;对于字符串,如果它使用的是8位的扩展ASCII字符集,那么它的基就是256。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
1、MSD 从高位开始进行排序
2、LSD 从低位开始进行排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
1、基数排序:根据键值的每位数字来分配桶
2、计数排序:每个桶只存储单一键值
3、桶排序:每个桶存储一定范围的数值
import java.util.ArrayList;
/**
* 类说明:基数排序
*/
public class RadixSort {
public static int[] sort(int[] array) {
if (array == null || array.length < 2)
return array;
/*找出最大数*/
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
/*先算出最大数的位数*/
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
/*构建桶*/
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
/*按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,
每一轮排序都基于上轮排序后的结果*/
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
/*遍历原始数组,投入桶中*/
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
/*桶中的数据写回原始数组,清除桶,准备下一轮的排序*/
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
}