算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法表明着用系统的方法描述解决问题的策略机制。java
简单点说,算法就是解决问题的方法。确切来讲它是相对于计算机程序的,大多数状况并不与具体某一种编程语言有关,但今天咱们采用java语言实现算法示例。原谅我是一只Android小菜鸟,就算不原谅,你又能拿我怎么滴?哈哈,开个玩笑,回到正题,方法有千千万万种,相信你们王者荣耀上分也是想了好多办法,尝试不一样的位置,不一样英雄,每一个英雄不一样的打法。最后发现,毫无卵用(手动滑稽)。那是你没找到好的方法,好比你能够找代练啊,而咱们的主题算法的优劣主要取决于时间复杂度和空间复杂度。那么有人问了,什么是时间复杂度,什么又是空间复杂度呢?
git
时间复杂度是指执行算法所须要的计算工做量。咱们见得更多的是这样的写法,O(1)、O(n)。到底什么意思呢?我相信更多的小伙伴想要的是这样的解读方式。github
int a=1;
int b=2;
int c=a+b;复制代码
相似于这种,执行语句的频度均为1,即便有上千万条,时间复杂度仍是O(1),只不过执行时间是一个很大的常数。面试
for(int i=0;i<n;i++){
int a=1;
int b=2;
int c=a+b;
}复制代码
在一个规模为n的循环中,无论是n次,仍是n-1次,对于时间复杂度都是线性的,取n,记为O(n)。算法
int i = 1;
while (i <= n){
i = i*2;
}复制代码
这个可能理解上有点困难,但也很简单,每次执行i都会乘以2,其实就是2的x次幂小于等于n,求得x为log2n,因此时间复杂度O(log2n)。shell
for (i = 0; i < n; ++i){
for (j = 0; j < n; j++){
printf ("%d\n", j);
}
}复制代码
相似于这种在规模为n的循环中又嵌套了一层规模为n的循环,那么时间复杂度就为O(n^2),同理n的屡次幂就是多层嵌套。编程
空间复杂度是对一个算法在运行过程当中临时占用存储空间大小的量度。通常来讲咱们不考虑空间复杂度,大多状况下为O(1),像递归可能会达到O(n)。api
稳定性就是一组元素,其中有重复的元素,好比2113,咱们把前面的1记为1a,后面的1记为1b,那么原始数据为21a1b3,若是排序后重复元素的相对位置不变,那么,这个排序是稳定的。如上例子,排序完应该是1a1b23才是稳定的,而不是1b1a23。数组
排序算法是算法的入门知识,但思想能够用于不少算法中。什么是排序?排序就是将一组对象按照某种逻辑顺序从新排列的过程。其实在咱们的api中提供了不少优秀的排序算法,那咱们为何还要去学习它?缘由很简单,它属于入门,有助于你理解其它更高大上的算法,同时它也是咱们解决其余问题的第一步。懂了它,你又向大佬靠近了一步。最重要的是面试官看你排序算法这么6,内心想这个确定是个大佬,必定要留住他,到时候就是你装逼的时候了。bash
我都懒得说排序算法有几种了,由于我根本不知道,一种排序算法可能对应多种变体,此次我给你们介绍8种常见的经典排序算法。
直接插入排序很好理解,就是从一组元素中取一个元素(确定是有序的,就一个嘛,称有序元素组),而后在剩下的元素中每次取一个元素使劲地往有序的元素组插,插到你满意为止。
是否是很好理解?若是以为仍是有点抽象,没有关系,每一个算法,我都会分为3个步骤讲解,思想-拆解分析-java代码实现-运行结果。
为了方便起见,排序的原始数据为5201314,很正规,有重复元素,没毛病。这里给你们一个小意见,像碰到/2或者说*2这种,用位运算更佳哦,但本文为了好理解采用了前者,哈哈。
咱们在实现直接插入排序的时候每每取第一个元素成立有序元素组,而后它后面的元素一个一个疯狂插。
颇有层次感是否是?在插的时候也有小技巧的,要温柔,要循循渐进。由于每当咱们插入一个元素,都是有序的,有序的说明什么?越后面确定越大,因此咱们只要从后面开始比较就好了(你非要从前面插,我也没办法),咱们从后一直往前比较,直到碰到小于或等于插入的元素为止,而后咱们乖乖的插到它后面就好了。
public static void insertSort(int[] array) {
//从第2个开始往前插
for (int i = 1, n = array.length; i < n; i++) {
int temp = array[i];//保存第i个值
int j = i - 1;//从有序数组的最后一个开始
for (; j >= 0 && array[j] > temp; j--) {
array[j + 1] = array[j];//从后往前比较,大于temp的值都得后移
}
array[j + 1] = temp;//碰到小于或等于的数中止,因为多减了1,因此加上1后,赋值为插入值temp
}
System.out.println("直接插入排序后:" + Arrays.toString(array));
}复制代码
从代码的实现来分析,运用咱们刚刚学的知识,一般状况下最外层是一个n规模的循环,内部又有一个规模为n的循环,所以平均时间复杂度为O(n^2);最坏的状况是内部的循环所有走一遍,好比咱们插入了一个最小的值,所以最差时间复杂度为O(n^2);最好的状况就是里面的循环不用走,好比咱们插入了一个最大的值,所以最好时间复杂度为O(n)。其中空间复杂度为O(1)。因为咱们是碰到小于或等于的数才中止,因此并不影响重复元素的相对位置,所以直接插入排序是稳定的。
希尔排序也是插入排序的一种,是直接插入排序算法的一种更高效的改进版本。
希尔排序是基于插入排序的如下两点性质而提出改进方法的:
说了这么多,其实就是插得不够理想,根据直接插入排序的时间复杂度,最好的状况能够达到线性的程度,通常状况下,却不是这样的,每次插入一个,可能要移动大量数据,咱们但愿在执行直接插入前,可以尽可能的保持有序。
取一个增量d1<n,使得距离为d1的元素分在一组,每组进行直接插入排序,而后再取d2<d1,进行排序,直到全部元素都在一组,即增量为1。
public static void shellSort(int[] array) {
for (int n = array.length, d = n / 2; d > 0; d /= 2) {//取增量为长度的一半,每次减半,直到d=1,可是d=1必须得排序,所以最后的判断为d>0
for (int x = 0; x < d; x++) {//分组
for (int i = x + d; i < n; i += d) {//每组进行直接插入排序
int temp = array[i];
int j = i - d;
for (; j >= 0 && array[j] > temp; j = j - d) {
array[j + d] = array[j];
}
array[j + d] = temp;
}
}
}
System.out.println("希尔排序后:" + Arrays.toString(array));
}复制代码
希尔排序的最好与最坏时间复杂度同直接插入排序,平均时间复杂度为O(n^1.3),不要问我1.3怎么来的,这跟增量的取值有关系,空间复杂度为O(1),因为在最后一次直接排序前,通过分组排序,因此可能重复元素的相对位置会交换,所以它是不稳定的。
我记得当初老师让咱们写一个排序算法,我第一个想的就是这个,可能大多数都是这个?很厉害了是否是?至少也是有名的排序算法。
在一组元素中,暴力找出最小的元素与第一个位置的元素交换,而后从剩下的元素中,选取最小的,与第二个位置交换,以此类推。
public static void selectSort(int[] array) {
for (int i = 0, n = array.length; i < n; i++) {
int j = i + 1;
int temp = array[i];
int position = i;
for (; j < n; j++) {
if (array[j] < temp) {
temp = array[j];
position = j;
}
}
array[position] = array[i];
array[i] = temp;
}
System.out.println("简单选择排序后:" + Arrays.toString(array));
}复制代码
从代码上看,简单选择排序彷佛没有什么最好最坏的时候,老是这么暴力,时间复杂度老是为O(n^2),空间复杂度为O(1),因为每次取到最小值后都要与前面位置元素交换,所以破坏了元素的相对位置,因此它是不稳定的。
说堆排序以前,必须说一下堆的概念:
彻底二叉树中任一非叶子结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。而咱们这里取不小于,也称之为大根堆。
利用大根堆的性质,每次把元素组建堆,取出最大值,放入最后,直到最后一位,排序完成。
以此类推,直到完成最后一个,排序完成。
public static void heapSort(int[] array) {
//从第一个非叶子结点开始,建堆
int n = array.length;
int startIndex = (n - 1 - 1) / 2;
for (int i = startIndex; i >= 0; i--) {
maxHeapify(array, n, i);
}
//末尾与头交换,交换后调整最大堆
for (int i = n - 1; i > 0; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
maxHeapify(array, i, 0);
}
System.out.println("堆排序后:" + Arrays.toString(array));
}
/**
* 建立最大堆
*
* @param array 元素组
* @param heapSize 须要建立最大堆的大小,通常在sort的时候用到,由于最大值放在末尾,末尾就再也不纳入最大堆了
* @param index 当前须要建立最大堆的位置
*/
private static void maxHeapify(int[] array, int heapSize, int index) {
int left = index * 2 + 1;//左子节点
int right = left + 1;//右子节点
int largest = index;
if (left < heapSize && array[index] < array[left]) {
largest = left;
}
if (right < heapSize && array[largest] < array[right]) {
largest = right;
}
//获得最大值后可能须要交换,若是交换了,其子节点可能就不符合堆要求了,须要从新调整
if (largest != index) {
int temp = array[index];
array[index] = array[largest];
array[largest] = temp;
maxHeapify(array, heapSize, largest);
}
}复制代码
从代码直接看时间复杂度其实挺难,我这里直接给答案,堆排序的平均、最差、最坏时间复杂度都是O(nlog2n),由于它永远都是一个套路,对这个时间复杂度怎么来的,能够网上搜搜,我相信你是棒棒的。空间复杂度为O(1),由于它须要建堆还要从新调整堆,确定是无法保证元素的相对位置的,因此它是不稳定的。
冒泡排序能够想象一下,鱼吐泡泡,一个一个泡泡往上冒。
元素之间两两比较,最小数往上冒,或者最大数向下沉,直到排序完成。
咱们这里以往上冒为例。
public static void bubbleSort(int[] array) {
int n = array.length;
for (int i = 0; i < n - 1; i++) {
for (int j = n - 1 - 1; j >= i; j--) {
if (array[j + 1] < array[j]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
System.out.println("第" + i + "趟:" + Arrays.toString(array));
}
System.out.println("冒泡排序后:" + Arrays.toString(array));
}复制代码
若是你单纯从上面的代码看,平均、最好、最差时间复杂度都是O(n^2),若是看过其它文章的同窗可能会说最好时间复杂度应该是O(n),那是由于加入了标志位,具体代码我不贴了,空间复杂度是O(1),因为一直是两两比较,并无改变相对位置的操做,因此是稳定的。
选择一个基数,通常咱们选择第一个数,而后把大于该数的放右边,小于该数的放左边,而后分别对左右两边用一样的方法处理,直到排序结束。
public static void quickSort(int[] array) {
_quickSort(array, 0, array.length - 1);
System.out.println("快速排序后:" + Arrays.toString(array));
}
private static int getMiddle(int[] array, int low, int high) {
int tmp = array[low]; //数组的第一个做为基数
while (low < high) { //直到指针重合一趟完成
while (low < high && array[high] >= tmp) {
high--;
}
array[low] = array[high]; //找到比基数小的
while (low < high && array[low] <= tmp) {
low++;
}
array[high] = array[low]; //找到比基数大的
}
array[low] = tmp; //基数归位
System.out.println(Arrays.toString(array));
return low; //返回基数的位置
}
private static void _quickSort(int[] array, int low, int high) {
if (low < high) {
int middle = getMiddle(array, low, high); //基于第一个数将array数组进行一分为二
_quickSort(array, low, middle - 1); //左边进行递归排序
_quickSort(array, middle + 1, high); //右边进行递归排序
}
}复制代码
关于快速排序的时间复杂度,表示三言两语真的说不清楚,感兴趣的朋友能够翻阅相关书籍,有能力的能够本身证实- -!最好时间复杂度为O(nlog2n),状况为可以正好根据基数平均划分元素组,最差时间复杂度为O(n^2),状况为元素组呈正序或者逆序状态,平均时间复杂度为O(nlog2n),由于快速排序是递归进行的,须要牵涉到递归深度,空间复杂度为O(nlog2n),准确来讲是平均空间复杂度,因为快速排序在跟基数比较的时候,可能会交换而破坏了元素之间的相对位置,所以快速排序是不稳定的。
采用经典分治思想,将一个元素组划分多个有序的小元素组,而后将这些小元素组合并成一个有序的元素组。
public static void mergeSort(int[] array) {
sort(array, 0, array.length - 1);
System.out.println("归并排序:" + Arrays.toString(array));
}
private static void sort(int[] array, int left, int right) {
if (left < right) {
//找出中间索引
int center = (left + right) / 2;
//对左边数组进行递归
sort(array, left, center);
//对右边数组进行递归
sort(array, center + 1, right);
//合并
merge(array, left, center, right);
}
}
private static void merge(int[] array, int left, int center, int right) {
int[] tmpArr = new int[array.length];
int mid = center + 1;
int third = left;//third记录中间数组的索引
int tmp = left;//复制时用到的索引
while (left <= center && mid <= right) {
//从两个数组中取出最小的放入中间数组
if (array[left] <= array[mid]) {
tmpArr[third++] = array[left++];
} else {
tmpArr[third++] = array[mid++];
}
}
//剩余部分依次放入中间数组
while (mid <= right) {
tmpArr[third++] = array[mid++];
}
while (left <= center) {
tmpArr[third++] = array[left++];
}
//将中间数组中的内容复制回原数组
while (tmp <= right) {
array[tmp] = tmpArr[tmp++];
}
System.out.println(Arrays.toString(array));
}复制代码
因为归并排序就一个套路并且合并的时候是从左往右,所以不会破坏元素的相对位置,是稳定的,同时它的最好、最坏、平均时间复杂度都是O(nlog2n),简单来讲它是基于彻底二叉树的,其深度为log2n,每次合并操做都是一个n级规模,所以为nlog2n,而空间复杂度除了深度log2n之外,咱们还须要临时数组,所以空间复杂度为O(n)=O(n)+O(log2n)。
因为是递归,可能与理想输出有所差距。
将一组元素进行桶分配,啥意思?好比数字250,百位是2,十位是5,个位是0,而这些个位,十位等就是所谓的桶。
因为测试数据全是个位数,因此只要进行一次就结束了,若是有更高位的,将一直进行到最高位。
public static void radixSort(int[] array) {
int max = array[0];
final int length = array.length;
for (int i = 1; i < length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int time = 0;//数组最大值位数
while (max > 0) {
max /= 10;
time++;
}
int k = 0; //从新放入数组的索引
int n = 1; //位值,如1,10,100
int m = 1; //当前在哪一位
int[][] temp = new int[10][length]; //数组的第一维表示该位数值,二维表示具体的值
int[] order = new int[10]; //数组order[i]用来表示该位是i的数的个数
while (m <= time) {
for (int num : array) {
int lsd = (num / n) % 10;//获取该位的基数0-9
temp[lsd][order[lsd]] = num;
order[lsd]++;
}
for (int i = 0; i < 10; i++) {
if (order[i] != 0) {
for (int j = 0; j < order[i]; j++) {
array[k] = temp[i][j];//基于m位的从新放入数组中
k++;
}
}
order[i] = 0;//复位
}
System.out.println("第" + m + "位排序:" + Arrays.toString(array));
n *= 10;
k = 0;//复位
m++;
}
System.out.println("基数排序后:" + Arrays.toString(array));
}复制代码
直接给答案,最优时间复杂度为O(d(r+n)),最差时间复杂度为O(d(r+n)),平均时间复杂度为O(d(r+n)),空间复杂度为O(rd+n),其中r表明关键字基数,d表明长度,n表明关键字个数,因为是分配且从左往右,所以是稳定的。
代码已经贴出来了,因为测试数据确实比较简单,你们能够本身使用复杂的原始数据进行测试。
到这里,8种常见的排序算法介绍的差很少了,若是你内心想的是,卧槽,这么简单,我立刻能够在个人小本本上写出来,那么我也没白写这篇文章。若是你是一脸懵逼,我内心可能想的是,卧槽,居然没糊弄过去。哈哈,无论怎样,算法并非什么高端的东西,其实你每天都在写算法,只不过。。。嘿嘿。算法可能有高低之分,但适合本身的才是最好的。尽可能领会其中的思想,来完善你的算法吧。而这篇文章只不过是抛砖引玉,同我上篇文章带你领略clean架构的魅力,你看,是否是不少架构的文章?我发现我愈来愈自恋了,哈哈,但真的很但愿更强的大佬能分享学习心得,我彻底同意打赏这种形式,甚至是你的小圈圈,但也得用点心啊。小弟,我心是用了,可是能力可能不足,若有错误,麻烦提出来,我及时修改。最后,感谢一直支持个人人!诶,差点没坚持下来。
哦,对了,我猜小伙伴又要吐槽个人做图,其实我以为挺好的,不是吗?
Github:github.com/crazysunj/