要求:掌握排序算法的基本思想和时空复杂度的计算,同时在理解的基础上能够熟练默写冒泡、选择、插入、快排、归并等常用算法
冒泡排序
实现步骤:(以下描述均假设待排序数组长度为length,默认从小到大排序)
- 从index = 1开始,与index前一个位置的元素比较大小,如果indexEle < (index - 1)Ele,就交换它们的值。不管indexEle是否小于(index - 1)Ele,判断完成后index++
- 循环第一个过程,直到index >= length时退出循环
- 经历完第一步和第二步后,最大的元素已经被放在了数组末尾,此时再循环执行一二过程,待排序数组长度减一,直到数组完全有序时退出循环
基础版本实现:
private void sort() {
for (int end = array.length - 1; end > 0; end--) {
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
swap(begin, begin - 1);
}
}
}
}
优化思路:
- 可以维护一个sortIndex变量记录下最新的待排序数组的终点下标,减少循环次数
package com.firework.sort.compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/15
* 冒泡排序 最坏平均时间复杂度O(n^2),最好为O(n),即原本就有序时。稳定排序算法
*/
public class BubbleSort extends Sort {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
//考虑极端情况,数组完全有序时应只执行一次循环。即 soreIndex - 1后的值 <= 0
int sortIndex = 1;
for (int begin = 1; begin <= end; begin++) {
if (cmp(begin, begin - 1) < 0) {
swap(begin, begin - 1);
//更新排序位置标记
sortIndex = begin;
}
}
end = sortIndex;
}
}
}
Sort接口代码:
package com.firework;
/**
* @author .idea
* @date 2021/4/11
*/
public abstract class Sort {
protected Integer[] array;
public void sort(Integer[] array) {
if (array == null || array.length < 2) {
return;
}
this.array = array;
//调用由具体子类实现的排序算法
sort();
}
/**
* 由子类实现的排序算法e
*/
protected abstract void sort();
/**
* 根据传入的下标比较大小
*
* @param index1
* @param index2
* @return
*/
protected int cmp(int index1, int index2) {
return array[index1] - array[index2];
}
/**
* 直接比较传入值的大小
*
* @param ele1
* @param ele2
* @return
*/
protected int cmpByEle(Integer ele1, Integer ele2) {
return ele1 - ele2;
}
/**
* 交换指定下标的值
*
* @param index1
* @param index2
*/
protected void swap(int index1, int index2) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
}
选择排序
基本思想:
- 循环待排序数组找到最大值,再与待排序数组的最后一个数字交换,同时待排序数组长度减一
- 循环上述过程直到待排序数组长度为0,即数组完全有序
package com.firework.sort.compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/15
* 选择排序 最好最坏平均都为O(n^2),稳定排序
*/
public class SelectionSort extends Sort {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
//等号保证该算法稳定
if (cmp(maxIndex, begin) <= 0) {
maxIndex = begin;
}
}
swap(maxIndex, end);
}
}
}
堆排序(做为选择排序算法的优化)
算法思想:
- 本质上和选择排序算法是一样的,都是不断找到待排序数组中的最大值然后再与数组最后一个位置元素交换。
- 区别在于堆排序算法对待排序数组中寻找最大值过程的优化:以建立最大堆来代替遍历整个待排序数组
package com.firework.sort.compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/11
* 堆排序 最好最坏平均都是O(nlogn) 不稳定排序
* 照样是依次把最大的元素放在先放在一边,类似于选择排序,不同的是对寻找最大值过程的优化
*/
public class HeapSort extends Sort {
//记录堆剩下元素的个数
private int heapSize;
@Override
protected void sort() {
//heapSize初始长度就是数组长度
heapSize = array.length;
/*自下而上的下滤,只有非叶子结点才需要下滤,而非叶子节点个数为 n >> 1,对应最大下标是 (n >> 1) - 1,O(n)级别*/
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
/*时间复杂度为 n * logn = nlogn级别 */
while (heapSize > 1) {
//交换堆顶元素与堆尾元素
swap(0,--heapSize);
//下滤新的堆顶元素
siftDown(0);
}
}
/**
* 下滤(最大堆)
*/
private void siftDown(int index) {
int ele = array[index];
/*
* 到没有子节点为止,即到叶子节点时退出循环,或者找到index的合适位置后提前退出循环
* 完全二叉树性质:叶子节点个数 = (n + 1) / 2,非叶子节点个数 = n / 2
*/
while (index < (heapSize >> 1)) {
//进入循环之后一定存在左子节点,右子节点不一定
int indexCom = (index << 1) + 1;
int com = array[indexCom];
int right = indexCom + 1;
//如果右子节点存在则比较左右子节点大小更新com
if (right < heapSize && cmpByEle(array[right], com) > 0) {
indexCom = right;
com = array[indexCom];
}
//然后再比较com与ele的大小
if (cmpByEle(com, ele) > 0) {
array[index] = com;
index = indexCom;
} else {
break;
}
}
array[index] = ele;
}
}
归并排序
算法步骤:
- 先将数组递归分解成最小单元(单个元素)
- 然后再将这些最小单元递归合并到一起
- 核心就在于其中的合并,本质就是合并两个有序数组,不过需要注意的点是归并排序中的两个待合并数组是挨在一起的,合并之前需要先备份左边数组。
package com.firework.sort.compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/15
* 归并排序 最好最坏平均都为O(nlogn),空间复杂度O(n/2 + logn) = O(n),稳定排序
*/
public class MergeSort extends Sort {
@Override
protected void sort() {
sort(0, array.length);
}
/**
* 将begin到end范围内的元素 排序(左闭右开)
*
* @param begin
* @param end
*/
private void sort(int begin, int end) {
//元素个数只有一个时结束递归
if ((end - begin) < 2) {
return;
}
int middle = (begin + end) >> 1;
sort(begin, middle);
sort(middle, end);
//分成最小单元后再进行合并
merge(begin, middle, end);
}
/**
* 合并[begin,middle]、[middle,end]范围元素
*
* @param begin
* @param middle
* @param end
*/
private void merge(int begin, int middle, int end) {
//由于待合并的两个数组是挨在一起的,故备份左边部分
int leftBegin = 0;
int leftEnd = middle - begin;
int[] newArray = new int[leftEnd];
int rightBegin = middle;
int rightEnd = end;
//备份左边数组元素
for (int i = leftBegin; i < leftEnd; i++) {
newArray[i] = array[begin + i];
}
while (leftBegin < leftEnd) {
if ((rightBegin < rightEnd) && (cmpByEle(array[rightBegin], newArray[leftBegin]) < 0)) {
array[begin++] = array[rightBegin++];
} else {
array[begin++] = newArray[leftBegin++];
}
}
}
}
插入排序
算法思想:
- 类似于打扑克牌一样,不断将新的元素插入到有序序列中去,直到整个序列完全有序
基础算法:
for (int begin = 1; begin < array.length; begin++) {
int curr = begin;
while (curr > 0 && (array[curr] - array[curr - 1]) < 0) {
swap(curr,curr - 1);
curr--;
}
}
改进思路:
- 在基础算法中插入一个新元素时,是从后往前遍历寻找合适的插入位置。每发现一个位置的元素不符合要求就立即交换元素,发现下一个不符合又继续交换,而实际最终的目的只是为了找到合适的位置插入一次就好。因此我们可以考虑备份下待插入元素的值,寻找合适插入位置的过程中并不立即交换二者元素,只移动原有序数组中的元素同时记录下待插入位置即可。
private void sort() {
for (int begin = 1; begin < array.length; begin++) {
int curr = begin;
int currEle = array[curr];
while (curr > 0 && (currEle < array[curr - 1])) {
array[curr] = array[curr - 1];
curr--;
}
array[curr] = currEle;
}
}
再次思考:
- 插入排序的核心思想是在有序数组中找到合适的插入位置。对于合适插入位置的寻找可以采用二分搜索算法进行优化。
package com.firework.sort.compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/11
* 插入排序 最坏平均都为O(n^2),最好为O(n),效率与逆序对个数有关,稳定排序
*/
public class InsertionSort extends Sort {
@Override
protected void sort() {
/*有序数组部分利用二分查找优化算法(减少比较次数)*/
for (int begin = 1; begin < array.length; begin++) {
//备份待插入数据
int ele = array[begin];
int insertIndex = search(begin);
//移动数组元素
for (int i = begin - 1; i >= insertIndex; i--) {
array[i + 1] = array[i];
}
//插入数据到指定位置
array[insertIndex] = ele;
}
}
/**
* 二分查找到index位置元素的插入位置
*
* @param index
* @return
*/
private int search(int index) {
int begin = 0;
//有序数组长度为index,end的值设为有序数组长度
int end = index;
while (begin < end) {
int middle = (begin + end) >> 1;
if (cmp(index, middle) < 0) {
end = middle;
} else {
begin = middle + 1;
}
}
return begin;
}
}
希尔排序(底层排序算法基于插入排序)
算法思想:
- 希尔排序按照步长序列依次将数组分成对应的列,在列内部进行排序,当列数为1时,数组排序即完成。
- 例如:步长序列为{8,4,2,1},则依次划分成8列、4列、2列、1列排序
package com.firework.sort.compare_sort;
import com.firework.Sort;
import java.util.ArrayList;
import java.util.List;
/**
* @author .idea
* @date 2021/4/14
* 希尔排序 最好时间复杂度O(n),最坏到O(n^2)不等,取决于步长序列 不稳定排序
* 按照步长序列按列进行排序,期间逆序对个数逐渐减少,因此底层排序算法适合用插入排序
*/
public class ShellSort extends Sort {
@Override
protected void sort() {
//步长序列
List<Integer> stepSeqence = stepSequence();
//按照步长序列排序
for (Integer step : stepSeqence) {
sort(step);
}
}
/**
* 分成step列排序
*
* @param step
*/
private void sort(int step) {
/*插入排序算法对每一列进行排序*/
for (int column = 0; column < step; column++) {
/*以挪动替代交换来优化算法*/
for (int begin = column + step; begin < array.length; begin += step) {
int curr = begin;
int currEle = array[curr];
while (curr > column && currEle < array[curr - step]) {
array[curr] = array[curr - step];
curr -= step;
}
array[curr] = currEle;
}
}
}
/**
* 生成步长序列(不固定)
*
* @return
*/
private List<Integer> stepSequence() {
List<Integer> stepSequence = new ArrayList<>();
int step = array.length;
while ((step = step >> 1) > 0) {
stepSequence.add(step);
}
return stepSequence;
}
}
快速排序(重中之重)
算法步骤:
- 选取一个中心值(center)作为基准,比center大的数都放在右边,比center小的数都放在左边
- 在center的左边和右边各自再选择一个center值,重复第一个步
- 重复第一第二步骤,直到数组不可再分时即排序完成
/**
* @author .idea
* @date 2021/4/16
* 快速排序 最坏是O(n^2),最好和平均都是O(nlogn) 空间复杂度O(logn) 不稳定排序
*/
public class QuickSock extends Sort {
@Override
protected void sort() {
quickSort(0, array.length);
}
//左闭右开区间
private void quickSort(int begin, int end) {
//只有一个元素时return,排序完成
if ((end - begin) < 2) {
return;
}
int centerIndex = centerIndex(begin, end);
quickSort(begin, centerIndex);
quickSort(centerIndex + 1, end);
}
/**
* 部分排序并返回中间值的下标
*
* @param begin
* @param end
* @return
*/
private int centerIndex(int begin, int end) {
//选取第一个位置元素做为基准值
int centerEle = array[begin];
//由于是左闭右开区间,end--指向数组最后一个元素
end--;
while (begin < end) {
//先从后往前扫描
while (begin < end) {
if (array[end] > centerEle) {
end--;
} else {
array[begin] = array[end];
begin++;
break;
}
}
//再从前往后扫描
while (begin < end) {
if (array[begin] < centerEle) {
begin++;
} else {
array[end] = array[begin];
end--;
break;
}
}
}
//将center值放在中间
array[begin] = centerEle;
return begin;
}
}
非比较排序——计数排序
算法思想:
- 针对整数排序的场景,以数组最大值和最小值之间的自然数个数(包括边界)作为counts数组容量。
- 遍历待排序数组元素,统计各个整数的出现次数,在counts对应下标位置记录下出现次数
- 更新counts数组的值为出现次数的累计值
- 从后往前遍历元素,将元素放到新建的有序数组中的合适位置
package com.firework.sort.not_compare_sort;
import com.firework.Sort;
/**
* @author .idea
* @date 2021/4/14
* 计数排序(针对整数排序) 空间时间复杂度都为O(n + k),k指数组整数的取值范围 稳定排序
*/
public class CountingSort extends Sort {
@Override
protected void sort() {
//1、找出最值
int max = array[0];
int min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
// 2.开辟内存空间
int[] counts = new int[max - min + 1];
// 3.统计每个整数出现的次数 (integer - min)得到的就是integer元素在counts数组中的下标
for (Integer integer : array) {
counts[integer - min]++;
}
// 4.更新counts数组的值,计算累加次数
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
// 5.从后往前遍历元素,将它放到有序数组中的合适位置
int[] newArray = new int[array.length];
for (int i = array.length - 1; i >= 0; i--) {
newArray[--counts[array[i] - min]] = array[i];
}
// 6.将有序数组赋值到array
for (int i = 0; i < newArray.length; i++) {
array[i] = newArray[i];
}
}
}