白话算法设计分析

白话算法设计分析

前述: 本来不太想写这个话题的,算法应该是一种需要专研思考的东西。后面想想,记录也应该是成长的一部分。

tip: 记录没有太多做题记录,一般都是经典题型的思路
这也是为什么叫白话算法了,但这玩意吧,需要沉淀!
话不多说。依旧是从排序开始。这里应该需要掌握算法的时间、空间复杂度的求解,3个渐进符号,上界,下界,渐进的意思。

比较型排序算法

比较型,可以理解为,这种排序算法两两元素需要比较排序才能得到的。值得一提的是,这种排序算法的下界是nlgn,也就是说,任何基于比较的排序算法最好也就只能渐进O(nlgn),这里可以使用决策树模型证明,证明就自己体会吧。

插入排序

插入排序的思路也很简单,可以想象成打扑克,现在你手上初始有一张牌,然后从牌堆抓取一张,为保证手上的牌是有序的,你需要去比较现在手上的牌,然后确定插入的位置,直至牌堆空了。
Code

public static void INSERTION_SORT(Integer[] nums){
        for(int i = 1;i<nums.length;i++){
            int key = nums[i]; // 记录当前的位置
            int j = i-1; // 游标,向前游
            while(j>=0&&nums[j]>key){   // 首先不越界,然后对比手中的牌
                nums[j+1] = nums[j];    // 可以交换就换
                j=j-1; //继续游动
            }
            nums[j+1] = key;   //最后插入
        }
    }

这里需要注意的是,一般的算法都会介绍,序列都是以1开头的,而这里还是使用的是0开头。一般我都会去刻意转换一下。
这里的思路需要知道,这里并没有一直比较在交换,而是看见合适的地方就先交换再说。最后在一个总的交换。慢慢体会一下。相比于确定位置在交换的话,这种不需要头痛的考虑数组越界问题。伪代码可以参考一下算法导论的代码,如果没记错出处的话。
时间,空间复杂度分析

相信这个都比较熟悉,首先是交换的1*c 这里的c是一个常数因子,而循环的话需要看循环的次数,这里的话是n,和 i 次,这里的i随着n变化而变化,看一下应该会是一个上三角矩阵的类型图,常数因子依旧是一个c这里的c很可能和上面的不同,不过影响算法的复杂度不会是他,但也有时候会是他,后面就有一些案例追求极致。
按照取大头的思路,可以得到渐进n^2,我不知道为什么这个符号打不出,但不是想当然的O(n^2),后面平均也可以看作是O(n^2)
空间复杂度:因为是原址排序,所以是O(1) 的
总结一下,时间:O(n^2) 空间:O(1)
后续的分析将快速带过

冒泡排序,选择排序不做赘述,他们的复杂度与插入差不多,也就是个常数因子的事。

归并排序

要了解归并排序就必须要掌握分治技术,而一般的分治都是递归实现,这里的递归需要练上百遍甚至千遍万遍,原因无他,就是很重要。循环可能都可以掌握,递归需要明确自己在做是什么,后面再补。回到正题。
分治: 首先就是需要先将问题划分成多个子问题,然后求解子问题,最后合并子问题的解得到问题解。这里和后面的动态规划很像,需要区别开。

归并排序的思路就是先将这个序列化划分成两个平均的子序列,这里有可能长度不等,不过没关系,然后排序子问题,最后组合起来,以此类推,要获得子问题的解,就需要子子问题的解,这就构成了递归。

Code

public static void MEGER_SORT(Integer[] nums,int left,int right){
        if(left<right){
            int mid = left+((right-left)>>1);
            MEGER_SORT(nums,left,mid);
            MEGER_SORT(nums,mid+1,right);

            // 合并
            Meger(nums,left,mid,right);
        }
    }

    private static void Meger(Integer[] nums,int left,int mid,int right){
        int p1 = left;
        int p2 = mid+1;
        Integer[] helper = new Integer[right-left+1];
        int i = 0;
        while(p1<=mid && p2<=right){
            helper[i++] = nums[p1]<nums[p2]?nums[p1++]:nums[p2++];
        }

        while(p1<=mid){
            helper[i++] = nums[p1++];
        }

        while(p2<=right){
            helper[i++] = nums[p2++];
        }

        for(i = 0;i<right-left+1;i++){
            nums[left+i] = helper[i];
        }

    }

解释就是先定义base element也就是基本情况,然后求解,这也就是递归出口,这里可以改成 left == right 然后直接返回就好。
这种情况就是两个下标指向同一个数,所以就是已经排好的序列。
这里在提一嘴,对于排序问题可以总结为:
有一个S{a1,a2…an}是无序的,需要返回一个序列使得ai<=aj,i<j;
而当没有排好的情况,我就先将他们分为平均的两部分,然后分别求解两个子问题,最后合并。
处理递归问题,必须要清除,这个函数的作用是什么,这样才能不晕。合理使用递归。而且,递归属于自顶向下的处理方式,他会避开一些不必要的子问题,也就是只会处理必要的子问题,而自底向上就不行,这是优势,但劣势很明显,就是有重叠子问题也会一直重复解。后话后话。
归并排序并没有重叠子问题,每要求解一个子问题的时候,都会带出新的子问题,所以不难看出,子问题的个数是n,递归树是lgn,所以他的时间复杂度是O(nlgn).
在使用递归的时候,不需要多想,只要清楚宏观上的解法,细节递归都帮你做了。归并就是,大的拆成两个小的左右,分别求左,求右,最后合并,不需要考虑细节是什么。
有了base element就OK。
处理base element,合并
这里使用的直接就是双指针,算法导论没记错的话是使用哨兵和准确的工作步长做的。各有各的好处,基本都只是常数因子的不同。
值得注意的是,这里使用了辅助空间,而且是n的,他的空间复杂度是O(n),需要注意的的是这个排序算法是稳定的。

为了体现常数因子的处理效果,加一个顺序统计量进来

这里就用一个顺序统计量的知识点,在序列中返回最大值和最小值。
返回最大值
Code

private static int maximum(Integer[] nums){
        if(nums == null || nums.length <=0){
            throw new NullPointerException("无数据");
        }
        int key = nums[0];
        for(int i = 1;i<nums.length;i++){
            if(nums[i]>key){
                key = nums[i];
            }
        }
        return key;
    }

返回最小的话,也是差不多的,所以他们的时间复杂度都是O(n)
然后这里需要同时返回最大最小的元组,所以,这里需要比较2(n-1)次,步骤是,当前的与最小的比,是否替换,当前的与最大的比,是否替换,而总共需要遍历(n-1)个元素。
这里微调一下,首先判断数组的长度是奇数还是偶数,奇数的话默认最大最小都是第一个元素,如果是偶数,先将前两个作比较,然后大的为最大,小的为最小。
后面都是两个两个遍历。先将两个比较一下,大的和最大值比,小的和最小值比。
神奇的事就发生了。他们的比较次数是3/2(n-1)+c
可以想象一下,确定2个数的情况,第一种需要4次,而后者仅需要3次,这就是常数因子的决策结果。他们都是渐进O(n)的
Code

public static void getMinAndMax(Integer[] nums){
        if(nums == null || nums.length <=0){
            throw new NullPointerException("无数据");
        }
        int begin = 1;
        int max = nums[0];
        int min = nums[0];

        if(nums.length%2==0) {
            max = Math.max(nums[0], nums[1]);
            min = Math.min(nums[0], nums[1]);
            begin = 2;
        }
        for(int i = begin;i<nums.length;i+=2){
            int n1 = nums[i];
            int n2 = nums[i+1];
            int t_max = Math.max(n1,n2);
            int t_min = Math.min(n1,n2);
            if(t_max>max){
                max= t_max;
            }
            if(t_min<min){
                min = t_min;
            }
        }

        System.out.println("max : "+max+" min : "+min);
    }

回归正题,归并排序是分治策略很好的案例,而且分治思想是非常重要的。后话,反正需要好好琢磨琢磨。

堆排序

这里需要先了解一下堆这个数据结构,这个数据结构是属于很底层的数据结构了,运用的地方也是相当广发,操作系统中的调度算法中就有这个的存在。
这里不做赘述,可以把他想象成为一颗有约束的二叉树,这颗树和排序树有些相近,但是需要区别开。
这里使用数组来作为他的存储结构。
最小堆(小顶堆):每颗子树都要满足所有的孩子结点都要大于等于父节点,并且是一颗完全二叉树。
而堆排序就是运用这一个特点,只需要有两个维护机制就可以一直保持堆的特点。一个是insert,也就是插入,一个是维护,也就是保持特点。
因为底层存储结构是数组,所以,需要一个size来表示堆的大小,在size外面的则不属于堆。
Code

 private static void heapSort(Integer[] nums){
        for(int i = 0;i<nums.length;i++){
            insertHeap(nums,i);
        }
        int max_size = nums.length-1;
        while (max_size>0){
            NumberArrayUtil.swap(nums,max_size,0);
            updateHeap(nums,--max_size);
        }
    }

    // 建堆
    private static void insertHeap(Integer[] nums,int index){
        while(nums[index]>nums[(index-1)/2]){
            NumberArrayUtil.swap(nums,index,((index-1)/2));
            index = (index-1)/2;
        }
    }

    // 维护堆
    private static void updateHeap(Integer[] nums,int max_size){
        int index = 0;
        int left = 1;
        while(left<=max_size){
            int max_index = left+1<max_size&&nums[left+1]>nums[left]?left+1:left;
            max_index = nums[index]>nums[max_index]?index:max_index;
            if(max_index==index){
                break;
            }
            NumberArrayUtil.swap(nums,index,max_index);
            index = max_index;
            left = index*2+1;
        }
    }

两个维护特征就不解读了,只需要注意这里是从0开始,而不是1
如果是1开始的话,本身为i,则左孩子为2*i,而右孩子就是2*i+1,也就是left+1;
堆排序的时间复杂度是渐进O(nlgn)可以说他是理想状态下非常好的排序了,奈何需要事实说话,快排在测试中还是优于他的。这里需要涉及随机算法和随机算法分析,一言难尽。
时间复杂度O(nlgn) 空间复杂度O(1),最坏的也是一样。但常数因子不行哦。
堆这个数据结构可以搭建一个优先调度算法,其实就是维护一个优先队列,用堆来搭建优先队列是常见的。
Code

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : zushiye
 */

/**
 * 优先队列实现
 * insert(S,x) 元素x
 * insert(S,i,x) 具体位置插入
 * maximum(S) 返回 最大key的数据
 * extract-max(S) 返回 最大key的数据 并且删除这个数据
 *
 */
public class PriorityQueue<T> {

    static class Node<T>{
        public int key;
        public T data;

        public Node(int key, T data) {
            this.key = key;
            this.data = data;
        }

        public Node() {

        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", data=" + data +
                    '}';
        }
    }

    private Node<T>[] nodes;  // 数据
    private int heapSize = 0; // 堆大小

    public PriorityQueue(){
        nodes =  new Node[10];
    }

    public PriorityQueue(int len) {
        nodes = new Node[len];
    }

    public T maximum_data(){
        return nodes[0].data;
    }

    public int maximum_key(){
        return nodes[0].key;
    }

    // 新增数据
    public void insert(int key, T x){
        int index = heapSize++;
        insert(index,key,x);
    }

    // 原数据修改
    public void insert(int i,int key,T x){
        if(i>heapSize){
            throw new IndexOutOfBoundsException("堆越界异常");
        }
        if(i>=nodes.length){
            nodes = add_size();// 扩容
        }
        nodes[i] = new Node<>(key,x);
        build_heap(i);
        uphold_heap(i,heapSize);
    }

    public T extract_max(){
        if(heapSize == 0){
            throw new NullPointerException("无数据");
        }
        T result = nodes[0].data;
        swap(0,--heapSize);
        uphold_heap(0,heapSize);
        nodes[heapSize] = null;
        return result;
    }

    // 扩容
    private Node<T>[] add_size(){
        Node<T>[] newNodes = new Node[nodes.length*2];
        System.arraycopy(nodes, 0, newNodes, 0, nodes.length);
        return newNodes;
    }

    // 创建堆
    private void build_heap(int i){
        while(nodes[i].key>nodes[(i-1)/2].key){
            swap(i,(i-1)>>1);
            i = (i-1)/2;
        }
    }

    // 维护堆
    private void uphold_heap(int i, int heapSize){
        int left = 2*i+1;
        while(left<heapSize){
            int max_index = left+1<heapSize && nodes[left+1].key>nodes[left].key?left+1:left;
            max_index = nodes[max_index].key>nodes[i].key?max_index:i;

            if(max_index == i){
                break;
            }
            swap(max_index,i);
            i = max_index;
            left = i*2+1;
        }
    }

    private void swap(int i,int j){
        Node<T> temp = nodes[i];
        nodes[i] = nodes[j];
        nodes[j] = temp;
    }

    public void look_heap(){
        for(int i = 0;i<heapSize;i++){
            System.out.println(nodes[i]);
        }
    }
}

好的程序等于好的算法+好的数据结构。
优先队列怎么使用呢,这里的key就好比事件的优先值,而data就是事件,也称卫星数据。只需要维护一个最大堆就可以做到事件优先执行,插入了。

tip: 分析一下优先队列操作的时间复杂度。

快速排序

终于到了这个常用的排序了,这里不会直接使用创造者的第一代版本的代码,但也不会一下很复杂。相队来说,白话文。
首先,不上升到分治策略,只需要简单递归,第一版

快速排序,这个排序是无法见名知意的。但是不得不说,他真的很快。
首先,定义一个基准值,这里直接了当,就是最后一个为什么呢,后面就明白了。
其次,定义一个小于基准数区域的右边界,初始为-1(从零开始,所以-1)表示没有比基准小的了
然后遍历0~最后的前一位,如果小于基准的,就让右边界向右移动一位,然后交换两个数,知道遍历结束。
最后交换右边界后一位和基准位。
然后递归排序以基准数的左边和右边。

虽然在最后一步还是用到了分治,但懂得都懂。这里就没有什么大区域了,这也是为什么直接定在最后一位了,但这样是不是可能很糟糕呢,说不定呢。所以将基准元素放中间也是一样的,多定义一个大区域,初始还是没有,然后遍历数组,如果是基准下标,就直接跳过,继续处理。
Code_one

private static void quickSort(Integer[] nums,int low, int high){
        if(low<high){
            int mid = partition(nums,low,high); // 定下基准元素

            quickSort(nums,low,mid-1);// 左边

            quickSort(nums,mid+1,high);// 右边
        }
    }


    // 基准元素在所有元素一边
    private static int partition(Integer[] nums,int low, int high){
        /**
         * 核心思想:
         * 选定一个基准坐标
         * 选定左区域为空区域,这里为了简单,直接认为就是第一个元素为左区域
         * 然后遍历数组
         * 遇见小的,就将左区域扩大一个并且将这个小的放进这个区域里面,也就是交换当前的数据和小区域的数据
         * 如果大于的话就不需要管,反正后面将基准数与小区域后面的的一个数交换就可以了
         *
         * 也就是说,基准元素定下来的位置只和小区域有关
         */
        // 获取该元素在 low~high 中的 index !base case
        int base_element = nums[high]; // 基准坐标
        int left = low; // 小于的游标
        for(int i = low;i<high;i++){
            if(nums[i]<=base_element){
                NumberArrayUtil.swap(nums,left++,i);
            }
        }
        NumberArrayUtil.swap(nums,left,high); // 交换游标位置和 基准元素位置
        return left;
    }

这里的partition就是确定基准元素的位置的函数,返回的就是排好序的坐标。

Code_two

    // 基准元素在中间
    private static int partition_02(Integer[] nums,int low,int high){
        int mid = low+(high-low)/2;
        int base_element = nums[mid];
        int left = low-1;
        int right = high+1;
        for(int i = low;i<=high;i++){
            if(i == mid){
                break;
            }
            if(nums[i]<=base_element){
                left++;
                NumberArrayUtil.swap(nums,left,i);
            }else{
                right--;
                NumberArrayUtil.swap(nums,right,i);
            }
        }

        // 两种形式都可以
//        int i = low;
//        while(left<right&& i<=high){
//            if(nums[i] == base_element){
//                break;
//            }
//            if(nums[i]<=base_element){
//                left++;
//                NumberArrayUtil.swap(nums,left,i);
//            }else{
//                right--;
//                NumberArrayUtil.swap(nums,right,i);
//            }
//            i++;
//        }

        NumberArrayUtil.swap(nums,left,high); // 交换游标位置和 基准元素位置
        return left;
    }

所以,下面的就是常见的快速排序了,其实有一个更简单的方法,而且效果好于这个的代码,就是随机partition,实现非常简单,使用第一种的partition方法。
Code_three

 private static int random_partition(Integer[] nums,int low, int high){
        int index = new Random().nextInt(high+1-low)+low;
        NumberArrayUtil.swap(nums,index,high);// 交换
        return partition(nums,low,high);
    }

这里的NumberArrayUtil是自己写的算法调试工具包,函数作用就是交换两个数的位置。简简单单。
时间复杂度O(nlgn)空间复杂度O(1)
这还是最优的,最坏的话时间是O(n^2)
但就是比堆排序好,欸,好气好气。
基于比较的就结束了。
后面就是不是比较的了,这个下界证明的话,说实话,看一下决策树就明白了,数学证明然后就可以懂了。
主要就是基数排序,计数排序,桶排序。

后面会巩固递归和分治,这俩思路基本涵盖大部分问题,这两个不会,直接gg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BoyC啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值