我个人算法方面其实一般,但是本科和研究生阶段的底子至少都还在,工作之后虽然算法方面的工作内容很少,但是一直还是记挂着算法这方面的东西,也是借着博客,从基础开始,刷一遍leetcode,很多东西可能已经不比现在的在校学霸,所以如果有不够精美的地方,欢迎指正批评
基本算法这块安排是这样:按照算法导论的过程串一遍流程,基本的算法和数据结构全部手撕,高级设计(动规,贪心,平摊)以leetcode为主,图论单独拎出来说讲
其次扩展一下大数据处理中的算法,基本的算法和数据结构仍然手撕,但是更侧重讲而非推理,侧重应用而非细节,因为leetcode题多
leetcode尽量做到日更,算法讲解部分尽量也是一周两更,不耽误细读经典的一周两更,还是希望有大家的支持和鼓励啊!!!!!
点赞的都暴富,恭喜发财!
让我们从一个有意思的题目开始:
public class Recursion
{
public static void main(String [] args)
{
Recursion r = new Recursion();
r.doSomething(3);
}
public void doSomething(int n)
{
if (n > 0)
{
doSomething(n-1);
System.out.print(n);
doSomething(n-1);
}
}
}
首先问打印的结果到底是什么,其次就是时间复杂度是什么?
这也是一道今年字节跳动的面试题之一。
稍微有点基础的同学都能理解:递归不论是你感性的递推,还是以栈的方式思考,得出的结论都是时间复杂度,打印结果1213121,但是以栈的方式思考需要注意栈弹出的顺序和处理(这里就是打印)元素的顺序是不一样的。算法作为理解和优化计算机思考方式的途径之一,对程序员而言,是优化自身逻辑的必备技能。
我们不再去从零开始讲内容,默认受众至少了解一些基本的数据结构知识,那么算法导论的开始,就是排序算法。对于排序算法,我个人建议是全部手撕多遍直到能够默写。当然默写也是建立在理解之上的。至于怎么理解并记忆,死记硬背并不可取,其实你想系统记忆一件事情最终要的要素之一就是记忆点,算法本身也有他的记忆点,我们从记忆点入手,把所有排序算法都过一遍。
不先给总结,我希望你优先单独理解每一个排序
1、冒泡(C语言第一节课老师讲的内容)
/**
* 冒泡排序
*
* @param array
* @return
*/
public int[] bubbleSort(int[] array) {
if (array.length == 0) {
return array;
}
int temp = 0;
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j + 1] < array[j]) {
temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
return array;
}
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
记忆点:顺序便利两遍,两两比较
稳定性:两两比较,不存在相同元素换位置,因此稳定
2、直接插入排序和希尔排序
先体会直插排序的点,再加步长就变成希尔排序
直插排序:一个一个遍历,拿当前元素和之间的元素比,把当前元素放在合适位置,遍历的节点之前的元素已经排成有序的部分(还有一种将大元素右移取代交换的思路,可以减少temp空间的使用,不过整体过程是一样的),代码:
/**
* 直接插入排序(交换)
* @param array
* @return int[]
*/
public static int[] directInsertSort(int[] array) {
if (array.length == 0) {
return array;
}
int temp = 0;
for (int i = 0; i < array.length - 1; i++) {
for (int j = i + 1; j > 0; j--) {
if (array[j] < array[j - 1]) {
temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
return array;
}
最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
记忆点:顺序遍历一遍,当前元素在有序部分再遍历一遍
稳定性:二遍遍历本质还是冒泡,稳定
希尔排序:直插排序步长为一,给定不是1的步长,就是希尔,那么到底步长应该是多少合适呢? 总体思路是刚开始数组无序,步长大一点加速排序,数组部分有序之后,用小步长整理出最终结果
可以参考希尔排序的步长选择问题_weixin_42506330的博客-CSDN博客_希尔排序的步长怎么取
以一般折半步长为例,希尔排序代码:
/**
* 希尔排序
*
* @param array
* @return int[]
*/
public static int[] hillSort(int[] array) {
if (array.length == 0) {
return array;
}
int temp = 0;
for (int gap = array.length / 2; gap >= 1; gap /= 2) {
for (int i = 0; i < array.length; i++) {
for (int j = i + gap; j < array.length; j += gap) {
if (array[j] < array[j - gap]) {
temp = array[j];
array[j] = array[j - gap];
array[j - gap] = temp;
}
}
}
}
return array;
}
最佳情况:T(n) = O(n1.3) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n log2 n)
最佳最差和步长有关
记忆点:带步长的直插
稳定性:步长不同时,没法保证交换元素的位置,不稳定
3、选择排序
选择当前数组里最小的放在最开始,之后再选剩下的最小的,排在第二位,以此类推
/**
* 选择排序
*
* @param array
* @return int[]
*/
public static int[] selectSort(int[] array) {
if (array.length == 0) {
return array;
}
// 你可以新建一个数组,用来存放结果,但更合理的做法是利用一个指针,指向当前最小,这样只用O(1)而不是O(n)
for (int i = 0; i < array.length; i++) {
int pointer = i;
//用一个指针记录本次遍历最小元素位置
for (int j = i; j < array.length; j++) {
if (array[j] < array[pointer]) {
pointer = j;
}
}
int temp;
temp = array[i];
array[i] = array[pointer];
array[pointer] = temp;
}
return array;
}
最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
记忆点:顺序便利两遍,选择最小的往前扔(当然也可以选择最大的往后扔)
稳定性:整体找最小的过程其实是稳定的,但是在处理不用排序的元素时,打乱了原有的元素顺序(举例理解 2 2 1,选择一次变成1 2 2,实际上第一个2 变成了第二个2),所以不稳定
4、快速排序
臭名昭著的快排真的得倒背如流,倒着写才符合面试官要求(狗头)
/**
* 快速排序
*
* @param array
*/
public static void quickSort(int[] array, int left, int right) {
//容易采坑点:要处理left>right,这不光是输入参数本身的要求
//举例 1 3 2, 以1为基准,第一遍处理完还是1 3 2,但是end = 0;递归的时候end-1<0,会报错
if (left >= right) {
return;
}
int start = left;
int end = right;
int midValue = array[left];
while (start < end) {
//容易踩坑点:>= 和 <=
while (start < end && array[end] >= midValue) {
end--;
}
while (start < end && array[start] <= midValue) {
start++;
}
if (start < end) {
int temp;
temp = array[end];
array[end] = array[start];
array[start] = temp;
}
}
array[left] = array[end];
array[end] = midValue;
quickSort(array, left, end - 1);
quickSort(array, end + 1, right);
}
最佳情况:T(n) = O(n log2 n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n log2 n)
记忆点:倒背如流要什么记忆点
稳定性:左右递归局部交换,这些决定了快排的不稳定,快排要注意的一点是,只要左右递归都右元素,那么快拍时间复杂度就是固定的
继续恶心人的开始延申以下,以上是快拍的递归解法,以下是快排的非递归解法,实际上,递归本身就是利用栈去实现的,因此熟悉一点的,直接上栈,解法也不难,这里改写一下几个方法让非递归更清晰,同时简化一下递归版本:
/**
* 非递归用栈实现
*
* @param array
* @param low
* @param high
*/
public static void quickSortUsingStack(int[] array, int low, int high) {
Stack<Integer> stack = new Stack<>();
stack.push(low);
stack.push(high);
while (!stack.isEmpty()) {
int right = stack.pop();
int left = stack.pop();
int mid = divide(array, left, right);
if (left < mid - 1) {
stack.push(left);
stack.push(mid);
}
if (mid + 1 < right) {
stack.push(mid + 1);
stack.push(right);
}
}
}
/**
* 递归
*
* @param array
* @param left
* @param right
*/
private static void quickSort2(int[] array, int left, int right) {
if (left < right) {
int mid = divide(array, left, right);
quickSort2(array, left, mid - 1);
quickSort2(array, mid + 1, right);
}
}
private static int divide(int[] array, int left, int right) {
int mid = left;
while (left < right) {
while (left < right && array[right] >= array[mid]) {
right--;
}
while (left < right && array[left] <= array[mid]) {
left++;
}
if (left < right) {
swap(array, left, right);
}
}
swap(array, mid, left);
return left;
}
private static void swap(int[] a, int left, int right) {
int t = a[left];
a[left] = a[right];
a[right] = t;
}
可以看出,整个快拍使用递归或非递归就干了两件事情,第一找基准,第二分左右继续找基准,非递归也就是用栈实现了递归过程而已,也就是用while达到不停的入栈出栈。