public interface ISort {
//常规实现
void sort(int[] sources);
//有优化版的填写优化版
void sortOpt(int[] sources);
}
冒泡
冒泡排序。
时间复杂度O(n^2)
空间复杂度O(1)
稳定排序
原地
简单
交换次数O(n^2)
普通版和优化版
算法优点:稳定
算法缺点:所需时间太长,时间复杂度高,接近O(n²)
冒泡排序理论上总共要进行n(n-1)/2次交换
仅供学习,不适合正式使用
public final class BubbleSort implements ISort {
@Override
public void sort(int[] sources) {
int size = sources.length;
while (size-- > 0) { //1. 外层循环不断缩小 总个数
for (int i = 0; i < size; i++) { //内循环从 0 < size 这个不断减小的数
if (sources[i] > sources[i + 1]) { //交换
int tmp = sources[i];
sources[i] = sources[i + 1];
sources[i + 1] = tmp;
}
}
}
}
@Override
public void sortOpt(int[] sources) {
int size = sources.length;
//追加一个标记上次排序已经好了的位置
int mark = 0;
while (size-- > 0) {
boolean isNoReplace = true; //1. 通过内循环是否有交换动作,来跳出#1
for (int i = mark; i < size; i++) { //2. 标记已经排好了的位置,用于下次直接从那个位置开始即可 #2
if (sources[i] > sources[i + 1]) {
if (isNoReplace) { //1. 通过内循环是否有交换动作,来跳出#2
isNoReplace = false;
mark = i; //2. 标记已经排好了的位置,用于下次直接从那个位置开始即可 #1
}
int tmp = sources[i];
sources[i] = sources[i + 1];
sources[i + 1] = tmp;
}
}
if (isNoReplace) { //1. 通过内循环是否有交换动作,来跳出#3
break;
}
}
}
}
选择排序
选择排序。
时间复杂度O(n^2)
空间复杂度O(1)
不稳定排序
原地
简单
交换次数O(n)
算法来讲,因为数据交换最小的,大部分情况优于冒泡。
优化算法为一次找到最大和最小。
算法优点:移动数据的次数已知。
算法缺点:比较次数多;不稳定。
选择排序总共要进行n-1次交换
public final class SelectionSort implements ISort {
@Override
public void sort(int[] sources) {
int size = sources.length;
for(int i = 0; i < size - 1; i++) {
int minIndex = i;
//1.1 一趟之后,找到了min index
for (int j = i + 1; j < size; j++) {
if (sources[j] < sources[minIndex]) {
minIndex = j;
}
}
//1.2 把miniIndex放到前面来
if (minIndex != i) {
int tmp = sources[i];
sources[i] = sources[minIndex];
sources[minIndex] = tmp;
}
}
}
@Override
public void sortOpt(int[] sources) {
int size = sources.length;
for(int left = 0, right = size - 1; left < right; left++, right--) {
int minIndex = left;
int maxIndex = right;
//1.1 一趟之后,找到了min index & max index
for (int j = left; j <= right; j++) {
if (sources[j] < sources[minIndex]) {
minIndex = j;
}
if (sources[j] > sources[maxIndex]) {
maxIndex = j;
}
}
//1.2 把miniIndex放到前面来
if (minIndex != left) {
int tmp = sources[minIndex];
sources[minIndex] = sources[left];
sources[left] = tmp;
//因为上面这个交换动作,会把left和minIndex交换位置。
// 所以,如果我们的最大值刚好就是这个left,那已经被我们交换走了。所以得换到minIndex的位置
if (left == maxIndex) {
maxIndex = minIndex;
}
}
//1.3 把maxIndex放到后面去
if (maxIndex != right) {
int tmp = sources[maxIndex];
sources[maxIndex] = sources[right];
sources[right] = tmp;
}
}
}
}
插入排序
插入排序。
时间复杂度O(n^2) 但对于比较有序的数列,时间复杂度能大幅下降
空间复杂度O(1)
稳定排序
原地
简单
交换次数O(n^2)
算法优点:稳定,快。
算法缺点:比较次数不一定,比较次数越多,插入点后的数据移动越多(特别是当数据总量庞大的时候)。但用链表可以解决这个问题。
最好情况:序列已经是期望顺序了,在这种情况下,需要进行的比较操作需(n-1)次即可。
最坏情况:序列是期望顺序的相反序列,那么此时需要进行的比较共有n(n-1)/2次。
插入排序算法的时间复杂度平均为O(n^2)
public final class InsertSort implements ISort {
@Override
public void sort(int[] sources) {
int N = sources.length;
//从第2,i=1个元素开始。
for (int i = 1; i < N; i++) {
int cur = sources[i];
//从i-1开始,倒着跟前面的数比较
int r = i;
while (r > 0 && cur < sources[r - 1]) {
sources[r] = sources[r - 1];
r--; //关键是这个要放里面去
}
//如果r == i表示while里面没有出现小于的情况,则跳出了
if (r != i) {
sources[r] = cur;
}
}
}
@Override
public void sortOpt(int[] sources) {
}
}
shell排序
希尔排序。 插入排序的优化版。
时间复杂度O(n^(1.3-2))
空间复杂度O(1)
不稳定排序
原地
复杂
交换次数
时间复杂度:需要根据增量的选择来看,通常比O(n^2)要好
优势:最坏情况下和平均情况差不多,中小规模输入
劣势:大规模输入(比一般的O(n^2)要好) 不稳定
,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.
本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。
原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,
此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。
public class ShellSort implements ISort {
@Override
public void sort(int[] sources) {
int size = sources.length;
int gap = Math.max(1, size * 2 / 5); //gap任取
//不要求面试能直接写出来。
//关键点就是三层循环。
//最外层循环:gap的计算逻辑;
//第二层循环:从gap开始,到size的循环。
//第三层循环:根据步调gap,进行插入排序的一趟。
while(true) {
for (int i = gap; i < size; i++) {
int tmp = sources[i];
int r = i;
while (r -gap >= 0 && tmp < sources[r - gap]) {
sources[r] = sources[r - gap];
r -= gap;
}
sources[r] = tmp;
}
if (gap == 1) {
break;
}
//计算gap
gap = gap > 6 ? gap / 3 : 1;
}
}
@Override
public void sortOpt(int[] sources) {
throw new RuntimeException("no");
}
}
快速排序
快速排序。 迭代版本。
时间复杂度O(nlogn)
空间复杂度O(log(n)) 体现在函数栈
不稳定排序
原地
普通版和优化版
算法优点:稳定
一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),
且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。
所以,对稳定性没有要求的情况下,快速排序总是优于归并排序。
public final class QuickSort implements ISort {
@Override
public void sort(int[] sources) {
quickSort(sources, 0, sources.length - 1);
}
private void quickSort(int[] src, int left, int right) {
if (left > right) {
return;
}
int mark = partition(src, left, right);
quickSort(src, left, mark - 1);
quickSort(src, mark + 1, right);
}
private int partition(int[] src, int left, int right) {
int pivotVal = src[left];
int l = left; //记录左右指针
int r = right;
while (l < r) {
//因为起手坑在pivot的位置,故而先从右边开始。
// 如果右边小于基准值,把src[r]变成'坑', 交换到l的位置。l++,r不变。因为他是'坑'.
while (src[r] > pivotVal && l < r) {
r--;
}
if (l < r) {
src[l++] = src[r];
}
while(src[l] < pivotVal && l < r) {
l++;
}
if (l < r) {
src[r--] = src[l];
}
}
src[l] = pivotVal;
return l;
}
@Override
public void sortOpt(int[] sources) {
}
}
归并排序
时间复杂度O(nlogn)
空间复杂度O(n)
稳定排序
并非原地
算法优点:稳定
当有 n 个记录时,需进行 logn 轮归并排序,每一轮归并,其比较次数不超过 n,元素移动次数都是 n,因此,归并排序的时间复杂度为 O(nlogn)。
归并排序时需要和待排序记录个数相等的存储空间,所以空间复杂度为 O(n)。
归并排序适用于数据量大,并且对稳定性有要求的场景。
他适合超大型数据的,使用外部磁盘存储排序的方案。
改进点还有多路归并,现在是二叉归并。
//归并排序 递归版本
public final class MergeSortRecursive implements ISort {
private int[] src;
private int[] tmp;
@Override
public void sort(int[] sources) {
src = sources;
tmp = new int[src.length];
sort(0, sources.length - 1);
}
private void sort(int left, int right) {
//优化点2:当数组比较小时,可以使用插入排序,效率更高。
//if(right-left+1<16) insertSort(int[] arr,int left,int right);
if (left >= right) { //停止递归条件
return;
}
//divide 分
int mid = (left + right) >> 1;
sort(left, mid);
sort(mid + 1, right);
//conquer 合
//优化点1:若已经有序,则不需要归并。更适用于近乎有序的数组。
if (src[mid] > src[mid + 1]) {
merge(src, tmp, left, mid, right);
}
}
private static void merge(int[] src, int[] tmp, int left, int mid, int right) {
//记录下,我们需要merge的2个区间;
// 左边区间[left, mid];右边区间从[mid+1,right]
//t记录下我们塞入tmp数组的index
int l = left;
int r = mid + 1;
int t = 0;
//拷贝到一个临时数组中去;其实优化点当数据比较少的时候,可以针对这里进行插入排序;不使用tmp数组
//不断的从2边取放到tmp中,直到有一遍触底
while (l <= mid && r <= right) {
if (src[l] <= src[r]) {
tmp[t++] = src[l++];
} else {
tmp[t++] = src[r++];
}
}
//将剩余的一边仅需考入到tmp中
while (l <= mid) {
tmp[t++] = src[l++];
}
while (r <= right) {
tmp[t++] = src[r++];
}
//将tmp还原到src中
while (--t >= 0) { //此时tempIndex其实是前面我们存入的tmpSize。所以要先--
src[right--] = tmp[t];
}
}
@Override
public void sortOpt(int[] sources) {
}
}
/**
* 归并排序。 迭代版本
*/
public final class MergeSortIterative implements ISort {
private int[] src;
private int[] tmp;
@Override
public void sort(int[] sources) {
src = sources;
int size = src.length;
tmp = new int[size];
//外层循环从1开始;逐渐扩大??
for (int step = 1; step < size; step *= 2) {
int doubles = 2 * step;
//从底部开始往上合并
for (int start = 0; start < size; start += doubles) {
int mid = Math.min(size - 1, start + step);
int right = Math.min(size - 1, start + doubles - 1);
merge(src, tmp, start, mid, right);
}
}
}
private static void merge(int[] src, int[] tmp, int left, int mid, int right) {
//记录下,我们需要merge的2个区间;
// 左边区间[left, mid];右边区间从[mid+1,right]
//t记录下我们塞入tmp数组的index
int l = left;
int r = mid + 1;
int t = 0;
//拷贝到一个临时数组中去;其实优化点当数据比较少的时候,可以针对这里进行插入排序;不使用tmp数组
//不断的从2边取放到tmp中,直到有一遍触底
while (l <= mid && r <= right) {
if (src[l] <= src[r]) {
tmp[t++] = src[l++];
} else {
tmp[t++] = src[r++];
}
}
//将剩余的一边仅需考入到tmp中
while (l <= mid) {
tmp[t++] = src[l++];
}
while (r <= right) {
tmp[t++] = src[r++];
}
//将tmp还原到src中
while (--t >= 0) { //此时tempIndex其实是前面我们存入的tmpSize。所以要先--
src[right--] = tmp[t];
}
}
@Override
public void sortOpt(int[] sources) {
}
}