算法从放弃到放弃第一期

本文作者分享了个人的算法复习计划,包括重温基础算法、数据结构,通过LeetCode每日练习,以及深入理解排序算法,如冒泡、选择、插入、希尔和快速排序。文章详细分析了各种排序算法的时间复杂度和稳定性,并提供了部分算法的Java实现。此外,还提及了递归的概念和一个递归实例。
摘要由CSDN通过智能技术生成

我个人算法方面其实一般,但是本科和研究生阶段的底子至少都还在,工作之后虽然算法方面的工作内容很少,但是一直还是记挂着算法这方面的东西,也是借着博客,从基础开始,刷一遍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);
        }
    }
}

首先问打印的结果到底是什么,其次就是时间复杂度是什么?

这也是一道今年字节跳动的面试题之一。

稍微有点基础的同学都能理解:递归不论是你感性的递推,还是以栈的方式思考,得出的结论都是时间复杂度2^{n},打印结果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达到不停的入栈出栈。

5、归并排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值