千里之行始于足下。我个人认为,无论是前端也好,后端也罢,掌握一些基本的算法还是必不可少的。代码的实现只是思想的具现化,关键还在于思想,实现的语言可以千变万化,但是思想还是一力降十会,以不变应万变。
本文主要分享了冒泡排序,插入排序,选择排序,快速排序,归并排序五大排序,以及如何使用快排在O(n)内找到一个无序数组中的第k大元素。话不多说,先上图(图中罗列了常用的排序,主要讲述其中的五个高频的,其中三个简单的,两个稍复杂一点的)。所有源码均已上传至github:链接
排序数组:[2, 4, 1, 3, 6, 5]复制代码
冒泡排序
时间复杂度:最好O(n),平均O(n2),最差O(n2)
冒泡排序是最最基本的排序了。它的原理比较简单,每一次的冒泡操作都是对相邻数据的两两比较,看是否满足大小关系要求,不满足则互换,以此类推,下面是代码
private int[] bubbleSort(int[] arrays) {
//第一个循环遍历,第二个循环比较
for (int i = 0; i < arrays.length; i++) {
for (int j = i + 1; j < arrays.length; j++) {
if (arrays[i] > arrays[j]) {
int tmp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = tmp;
}
}
System.out.print("第" + i + "次交换");
printAll(arrays);
}
return arrays;
}复制代码
它的执行情况是这样的:
但是发现一个问题,做了好多重复操作,因为将代码改造如下(手动加了一个flag的标识位,手动跳出循环)
private int[] bubbleSort(int[] arrays) {
//第一个循环遍历,第二个循环比较
for (int i = 0; i < arrays.length; i++) {
//退出标记
boolean flag = false;
for (int j = i + 1; j < arrays.length; j++) {
if (arrays[i] > arrays[j]) {
int tmp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = tmp;
flag = true;
}
}
if (!flag) break;
System.out.print("第" + i + "次交换");
printAll(arrays);
}
return arrays;
}复制代码
再次执行,结果如下,明显少了几次循环
get:遇到问题多多考虑加标识
插入排序
时间复杂度:最好O(n),平均O(n2),最差O(n2)
插入排序的思想有点类似扑克牌思想。默认arrays[0]为第一张扑克牌,并且第一个循环从1开始,每次揭一张牌然后和手里得牌进行比较,插入相应的位置。
private int[] insertionSort(int[] arrays) {
for (int i = 1; i < arrays.length; i++) {
int value = arrays[i];
int j = i - 1;
for (; j >= 0; --j) {
if (value < arrays[j]) {
arrays[j + 1] = arrays[j];
} else {
break;
}
}
arrays[j + 1] = value;
System.out.print("第" + i + "次交换");
printAll(arrays);
}
return arrays;
}复制代码
它的执行情况是这样的:
选择排序
时间复杂度:最好O(n2),平均O(n2),最差O(n2)
选择排序的思想有点像插入排序,在第一个循环里,它都默认当前的值是最小,然后取出它的下标,在第二个循环里找到比自己还要小的,并赋值,然后再做交换操作,以此类推。它的代码实现如下:
private int[] selectionSort(int[] arrays) {
for (int i = 0; i < arrays.length; i++) {
int minIndex = i;
for (int j = i; j < arrays.length; j++) {
if (arrays[j] < arrays[minIndex]) {
minIndex = j;
}
}
int tmp = arrays[i];
arrays[i] = arrays[minIndex];
arrays[minIndex] = tmp;
System.out.print("第" + i + "次交换:");
printAll(arrays);
}
return arrays;
}复制代码
它的执行情况是这样的:
快速排序
时间复杂度:最好O(nlogn),平均O(n2),最差O(n2)
快排和归并排序可能要比前面三个排序要稍微复杂一点。这两种排序适合大规模数据的排序。
他们都用到了分治算法的思想:分而治之。
什么是分治算法?
分而治之。先分解再解决然后合并:
- 将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题
- 解决这些子问题
- 合并其结果,就得到原问题的解
快排的思想是取arrays数组中一组数组,取这个区间范围内任意一个数据作为它的分区点pivot,然后遍历数据将小的放在pivot左边,大的放在右边。
比较简单的实现就是如果空间足够,申请两个临时数组 A 和 B,遍历 arrays,将小于 pivot 的元素都拷贝到临时数组 A,将大于 pivot 的元素都拷贝到临时数组 B,最后再将数组A和数组B 中数据顺序拷回arrays。
但是这样就不是原地排序了,并且浪费空间,所以我们应该是原地分区,具体实现如下:
private void quickSort(int[] arrays, int head, int tail) {
if (head >= tail) return;
int pivot = partition(arrays, head, tail);
quickSort(arrays, head, tail - 1);
quickSort(arrays, pivot + 1, tail);
}复制代码
关键在于partition函数,它是用来获取分区点,并且交换元素。
这里注意一个细节,对分区点pivot的选择是当前arrays的tail位,而不是head位,这样的好处是会避免数据的重复交换,这个pivot的选择一定要注意,选择的不合理,时间复杂度会直接退化到O(n2)具体代码实现如下
private int partition(int[] arrays, int head, int tail) {
int pivot = arrays[tail];
int i = head;
for (int j = head; j < tail; j++) {
if (pivot > arrays[j]) {
int tmp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = tmp;
++i;
}
}
int tmp = arrays[i];
arrays[i] = arrays[tail];
arrays[tail] = tmp;
return pivot;
}复制代码
归并排序
时间复杂度:最好O(nlogn),平均O(nlogn),最差O(nlogn)
快排是自上而下先分区处理子问题后合并,而归并正合适相反,归并是自下而上先处理子问题再合并。归并的缺点是比快排占内存。
归并是思路是把需要排序的数组从中间分成两部分,对每一部分分别排序,排好序然后再合并在一起就可以具体代码如下:
private void mergeSort(int[] arrays, int head, int tail) {
if (head >= tail) return;
int pivot = (head + tail) / 2;
//分而治之
mergeSort(arrays, head, pivot);
mergeSort(arrays, pivot + 1, tail);
//合并
merge(arrays, head, pivot, tail);
}复制代码
关键点在于这个merge方法。
它声明了一个tmp数组用来存储分解的子数组,第一个while循环,遍历部分arrays数组,并且根据大小关系来移动p或者q,然后将满足划分关系的数据存入tmp。
private void merge(int[] arrays, int head, int pivot, int tail) {
System.out.println("head=" + head + ",pivot=" + pivot + "tail=" + tail);
int p = head;
int q = pivot + 1;
int k = 0;
int[] tmp = new int[tail - head + 1];
while (p <= pivot && q <= tail) {
if (arrays[p] <= arrays[q]) {
tmp[k++] = arrays[p++];
} else {
tmp[k++] = arrays[q++];
}
}
// 判断哪个子数组中有剩余的数据
int start = p;
int end = pivot;
if (q <= tail) {
start = q;
end = tail;
}
// 将剩余的数据拷贝到临时数组tmp
while (start <= end) {
tmp[k++] = arrays[start++];
}
// 将tmp中的数组拷贝回arrays
// for (int i = 0; i <= tail - head; ++i) {
// arrays[head + i] = tmp[i];
// }
//上述代码与下面代码等同,只不过idea有个警告,我就转换了一下
if (tail - head + 1 >= 0)
System.arraycopy(tmp, 0, arrays, head, tail - head + 1);
}复制代码
并且我将每一次执行merge方法的时候,head,piovt,tail的执行情况打印了出来。
排序测试结果
public static void main(String[] args) {
BaseSort baseSort = new BaseSort();
int[] arrays = {2, 4, 1, 3, 6, 5};
int[] bubbleRes = baseSort.bubbleSort(arrays);
System.out.print("冒泡排序:");
baseSort.printAll(bubbleRes);
int[] insertRes = baseSort.insertionSort(arrays);
System.out.print("插入排序:");
baseSort.printAll(insertRes);
int[] selectRes = baseSort.selectionSort(arrays);
System.out.print("选择排序:");
baseSort.printAll(selectRes);
int[] quickArray = {2, 4, 1, 3, 6, 5};
baseSort.quickSort(quickArray, 0, quickArray.length - 1);
System.out.print("快速排序:");
baseSort.printAll(quickArray);
int[] mergeArray = {2, 4, 1, 3, 6, 5};
baseSort.mergeSort(mergeArray, 0, mergeArray.length - 1);
System.out.print("归并排序:");
baseSort.printAll(mergeArray);
}复制代码
如何使用快排在O(n)内找到一个无序数组中第k大元素
分析:快排的思想是分而治之,还是以arrays{2,4,1,3,6,5}为例,当k=3时,期望结果是4,这里的关键还是分区点的选择,这里以arrays最后一个元素的下标作为pivot。这样数组就分成了三部分,然后再遍历查找k的值所在的位置即可。具体实现代码如下:
private int findKByQuick(int[] arrays,int head,int tail,int k){
int pivot = partition(arrays, head, tail);
if(pivot > k - 1){
return findKByQuick(arrays,head,pivot - 1,k);
}else if (pivot < k - 1){
return findKByQuick(arrays,pivot + 1,tail,k);
}
// pivot == k - 1
System.out.println("k = " + (pivot + 1));
printAll(arrays);
return arrays[pivot];
}复制代码
关键点还在于分区函数,这里将常规排序的分区函数改造了一下,返回下标即可。
private int partition(int[] arrays, int head, int tail) {
int last = arrays[tail];
int pivot = head;
for (int j = head; j < tail; j++) {
if (last < arrays[j]) {
int tmp = arrays[pivot];
arrays[pivot] = arrays[j];
arrays[j] = tmp;
++pivot;
}
}
int tmp = arrays[pivot];
arrays[pivot] = arrays[tail];
arrays[tail] = tmp;
return pivot;
}复制代码
测试结果
根据测试结果可以得知,利用快排查找第K大元素并没有将数组完全排序,而是只进行了部分排序,因此时间复杂度为O(n)。
public static void main(String[] args) {
FindKthLargest findKthLargest = new FindKthLargest();
int[] findArray = {2, 4, 1, 3, 6, 5};
int k = 2;
int kValue = findKthLargest.findKByQuick(findArray,0,findArray.length - 1,k);
System.out.print("第" + k + "大元素为" + kValue);
}复制代码
end
您的点赞和关注是对我最大的支持,谢谢!