算法基础-十大排序算法
排序就是将一组对象按照某种逻辑顺序重新排列的过程。
一、冒泡排序
从数组头开始,比较相邻的元素。如果第一个比第二个大,就交换。
/**冒泡排序(升序)*/
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] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
return array;
}
二、简单选择排序
从待排序的元素中选出最小(大)元素与第1个元素交换,执行n-1趟后就完成了排序。这种方法叫做选择排序,因为不断地选择剩余元素中最小(大)元素。是冒泡排序的一种优化,减少了交换次数。
/**简单选择排序(升序)*/
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 + 1; j < array.length; j++) {
if (array[minIndex] > array[j]) {
minIndex = j;
}
}
//交换最小数和i当前所指的元素
if(minIndex != i){
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
}
return array;
}
三、简单插入排序
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。
/**简单插入排序(升序)*/
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];
/*在已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
将比较的元素元素后移一位*/
while (preIndex >= 0 && currentValue < array[preIndex]) {
//将当前元素后移一位
array[preIndex + 1] = array[preIndex];
preIndex--;
}
//while循环结束时,说明已经找到了当前待排序数据的合适位置插入
array[preIndex + 1] = currentValue;
}
return array;
}
四、希尔排序
一种基于插入排序的快速的排序算法。希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是突破O(n^2)的第一批算法之一。希尔排序是把待排序数组按一定数量的分组,对每组使用直接插入排序算法排序;然后缩小数量继续分组排序,随着数量逐渐减少,每组包含的元素越来越多,当数量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的数量,就构成了一个增量序列。
/**希尔排序(升序)*/
public static int[] sort(int[] array) {
int len = array.length;
/*按增量分组后,每个分组中,currentValue代表当前待排序数据,该元素之前的元素均已被排序过*/
int currentValue;
//gap指用来分组的增量,会依次递减
int gap = len / 2;
while (gap > 0) {
for (int i = 0; i < len - gap; i++) {
//组内已被排序数据的索引
int preIndex = i;
currentValue = array[i + gap];
/*在组内已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
并将比较的元素元素在组内后移一位*/
while (preIndex >= 0 && array[preIndex] > currentValue) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
//while循环结束时,说明已经找到了当前待排序数据的合适位置插入
array[preIndex + gap] = currentValue;
}
gap /= 2;
}
return array;
}
在先前较大的增量下每个子序列的规模都不大,用直接插入排序效率都较高,尽管在随后的增量递减分组中子序列越来越大,由于整个序列的有序性也越来越明显,则排序效率依然较高。
五、归并排序
对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
/**归并排序*/
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++];
}
}
return result;
}
六、快速排序
首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数(Pivot),然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
/**
* 快速排序算法-前后指针
*
* @param array
* @param start
* @param end
* @return
*/
public static void partition(int[] array, int start, int end) {
int pivot = (int) (start + Math.random() * (end - start + 1));
int low = start;
int high = end;
while (low < high) {
//找到第一个比基准数大的
while (array[high] > array[pivot]){
high--;
}
//找到第一个比基准数小的
while (array[low] < array[pivot]) {
low++;
}
swap(array, low, high);
}
//函数递归调用,实现数组分解。直至start=low,end=high
if (low > start) {
sort(array, start, low - 1);
}
if (high < end) {
sort(array, high + 1, end);
}
}
/**
* 交换数组内两个元素
*
* @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 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);
}
return array;
}
/**
* 快速排序算法-分割指示器
*
* @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));
/*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);
}
}
}
return zoneIndex;
}
七、堆排序
所谓二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点;在一个二叉堆中,根节点总是最大(或者最小)节点。
堆排序算法就是抓住了这一特点,每次都取堆顶的元素,将堆顶元素与尾元素进行交换,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。
完全二叉树特性:
- 对于位置为K的结点,其左子结点=2k+1 ,其右子结点=2(k+1)。
- 后一个非叶节点的位置为 (N/2)-1,N为数组长度。
/**声明全局变量,用于记录数组array的长度*/
private static int len;
/**
* 堆排序(升序)
*
* @param array
* @return
*/
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);
}
return array;
}
/**
* 建立最大堆
*
* @param array
*/
public static void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
for (int i = (len / 2 - 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
/**
* 调整使之成为最大堆
*
* @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;
}
//如果有右子树,且右子树大于maxIndex,则将最大指针指向右子树
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以内。对于数组里每个元素来说,如果能知道数组里有多少项小于或等于该元素,就能准确地给出该元素在排序后的数组的位置。
计数排序对一定范围内的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序,而且待排序元素值分布较连续、跨度小的情况。
/**
* 计数排序(升序)
*
* @param array
* @return
*/
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]++;
}
//访问原始数组时的下标计数器
int index = 0;
//访问计数数组时的下标计数器
int i = 0;
//访问计数数组,将计数数组中的元素转换后,重新写回原始数组
while (index < array.length) {
//只要计数数组中当前下标元素的值不为0,就将计数数组中的元素转换后,重新写回原始数组
if (counterArray[i] != 0) {
array[index] = i - bias;
counterArray[i]--;
index++;
} else{
i++;
}
}
return array;
}
九、桶排序
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。
/**
* 桶排序
*
* @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 < bucketCount; i++) {
if (bucketSize == 1){
//已经分化到最小,将当前桶的数据依次放入结果
for (int j = 0; j < bucketArr.get(j).size(); j++) {
resultArr.add(bucketArr.get(i).get(j));
}
} else {
//说明bucketSize过大*,需要减少size
if (bucketCount == 1) {
bucketSize = bucketSize / 2;
if (bucketSize == 0) {
bucketSize += 1;
}
}
//对每个桶中的数据再次用桶进行排序,递归结束的标志是bucketSize == 1
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数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,同时每一轮排序都基于上轮排序后的结果;当我们将所有的位排序后,整个数组就达到有序状态。基数排序不是基于比较的算法。
/**
* 基数排序(升序,基数为10)
*
* @param array
* @return
*/
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;
}
十一、排序方法对比
11.1 算法的复杂度
算法的复杂度往往取决于数据的规模大小和数据本身分布性质。时间复杂度: 一个算法执行所耗费的时间。空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度。常见复杂度由小到大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
在各种不同算法中,若算法中语句执行次数(占用空间)为一个常数,则复杂度为O(1);当一个算法的复杂度与以2为底的n的对数成正比时,可表示为O(log n);当一个算法的复杂度与n成线性比例关系时,可表示为O (n),依次类推。
时间复杂度记忆:
-
冒泡、选择、插入排序需要两个for循环,每次只关注一个元素,平均时间复杂度为(一遍找元素O(n),一遍找位置O(n))
-
快速、归并、堆基于分治思想,log以2为底,平均时间复杂度往往和O(nlogn)(一遍找元素O(n),一遍找位置O(logn))相关
-
希尔排序依赖于所取增量序列的性质,但是到目前为止还没有一个最好的增量序列 。例如希尔增量序列时间复杂度为O(n²),而Hibbard增量序列的希尔排序的时间复杂度为 , 有人在大量的实验后得出结论;当n在某个特定的范围后希尔排序的最小时间复杂度大约为n^1.3。
11.2 算法的稳定性
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。
11.3 总结
从平均时间来看,快速排序是效率最高的:
快速排序中平均时间复杂度O(nlog n),这个公式中隐含的常数因子很小,比归并排序的O(nlog n)中的要小很多,所以大多数情况下,快速排序总是优于合并排序的。
虽然堆排序的平均时间复杂度也是O(nlog n),但是堆排序存在着重建堆的过程。它把堆顶与尾元素交换后需要重建堆,尾元素与它的两个叶子结点相差很多,一般要比较很多次才能回到合适的位置。堆排序有很多的时间耗在堆调整上。
虽然快速排序的最坏情况为排序规模(n)的平方关系,但是这种最坏情况取决于每次选择的基准, 对于这种情况,已经提出了很多优化的方法,比如三取样划分和Dual-Pivot快排。同时,当排序规模较小时,划分的平衡性容易被打破,而且频繁的方法调用让时间复杂度超过了O(nlog n),为省出的时间,一般排序规模较小时,会改用插入排序或者其他排序算法。