算法-从入门到放弃

算法很重要也很难这个众所周知,不知是幸运还是不幸,现在的企业面试也开始卷算法了。我自己是没什么算法底子,老实说现在让我手写个简单排序我都写不好的,就从这里开始吧。

先放代码地址,拷了好多别人的资料,老实说看的一脸懵,会慢慢删掉别人的代码,自己再抄一遍的

algorithm: 算法学习及练习

一、等差数列和等比数列

本人比较懒,觉得学算法最重要的就是“领会精神”,搞懂了算法的思想,代码也就是熟能生巧的事,反正会忘谁在乎呢。那么就从大家都知道的,高中数学里两个两个有意思的推理说起吧。

等差数列求和这个嘛,有一个很有名的从1加到100的故事[(1+100)*100/2],公式大概都能脱口而出了(首项+尾项)*项数/2,表达式为(n^2+n)/2

重点说说等比数列等比数列求和,额...还是上链接吧你应该想想,等比数列的求和是怎么来的? - 知乎

简诉推导过程如下:a是首项、q是公比
Sn= a + a1*q + a*q^2 + ... + a*q^n-1;
Sn*q= a1*q + a*q^2 + ... + a*q^n-1 + a*q^n
相减得:
Sn*q - Sn = a*q^n - a
Sn*(q-1) = a*(q^n - 1)
Sn = a*(q^n - 1)/(q-1)

其实就是巧妙的使用了错位相减的方法,算法其实同数学思想一样只不过是换了个名字而已,都是找规律,通过正向的逆向的规律来完成数据的批量处理。

二、从两数之和到abandon

话说某一天兴冲冲的打开leetcode准备刷算法,然后迎面而来的就是两数之和,两个月后又兴冲冲的打开leetcode,没错还是两数之和^_^,就想起了我大学时兴冲冲的说我要考研,背了俩月英语单词,翻来覆去的abandon,最后真就abandon了……

能放到简单难度的第一题,还是有他独特的魅力的。首先这个题目足够简单,求和嘛,两层循环挨个加加到刚好等于目标值为止,别说这方法low总比写不出来好。
某人就这么写完开始看评论区,然后就被卷佬们惊到了,好得是求和,甭管怎么算不是加法就是减法吧,都不是!大佬们连加减乘除都是从位运算写起,我算知道我算法怎么老刷老第一题了,太特么劝退了。

就不废话了,这题我从评论区抄了两个解法,

第一个:工程法,就是类似问题在我们日常工作中通常会使用的方法,就是一次运算[初始化]把所有可能性缓存起来[map/redis],等用的时候直接从缓存取,第一次O(n^2),后面O(1)

第二个:比赛法,就是要速度更快内存更小,时间复杂和空间复杂都尽可能好的方法。还是逆向思维,比如1+2=3,3是target,当前如果是1,其实我们就是在找2,2就是所谓的补数,只要每次把补数及其下标存起来,在遍历到队末之前,我们必然能找到“一对”符合target的数字。时间复杂度和空间复杂度都要比第一个方法小一个量级,了解这种写法才是我们学算法的目的——领会她的思想。

    int[] no = {3,2,4};
    int target = 6;
    @Test
    public void twoSum1(){
        //工程缓存法
        Map map = new HashMap();
        for (int i = 0; i < no.length-1; i++) {
            for (int j = i+1; j < no.length; j++) {
                if(i != j){
                    map.put(no[i]+no[j], new int[]{i,j});
                }
            }
        }
        int[] index = (int[])map.get(target);
        System.out.println(Arrays.toString(index));
    }

    @Test
    public void twoSum2(){
        //补数 逆向思维
        Map<Integer, Integer> map = new HashMap();
        int[] index = new int[2];
        for (int i = 0; i < no.length; i++) {
            if(map.containsKey(no[i])){
                index[0] = map.get(no[i]);//map k补数,匹配的数;  v该数的下标
                index[1] = i;
                break;
            }
            map.put(target - no[i], i);
        }
        System.out.println(Arrays.toString(index));
    }

三、数组排序

数组排序主要讲了菜鸡三兄弟[冒泡、选择、插入]和NB三兄弟[快排、堆排、归并排序],代码上面的git都有的,为了凑篇幅,这边就单独贴出来,随便搭个java环境就可以运行这段代码。

package com.cheng.algorithm;

import org.junit.Before;
import org.junit.Test;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;


public class 排序 {

    private static final int cycleNo = 10;
    private static final int maxArrayLength = 1500;
    private static final int maxNo = 100;

    @Before
    public void init (){

    }

    /**
     * 排序结果测试
     * 总结:三大nlogn排序
     * 堆排:理解复杂,速度相对较慢[无意义的交换较多],在工程实践中,其自动排序的思想很有用
     * 快排:理解中等,速度相对最快[受数据情况影响,最坏情况时最慢],在拆的过程中完成排序
     * 归并排序:理解较简单,速度稳定[占用内存],在合的过程中完成排序
     */
    @Test
    public void test(){
        for (int i = 0; i < cycleNo; i++) {
            int[] arr = getRandomArray();
            int[] arr1 = copyArray(arr);
            int[] arr2 = copyArray(arr);
            int[] arr3 = copyArray(arr);
            int[] arr4 = copyArray(arr);
            int[] arr5 = copyArray(arr);
            int[] arr6 = copyArray(arr);
            testSort(t->bubble(arr), arr);
            testSort(t->selection(arr1), arr1);
            testSort(t->insert(arr2), arr2);
            testSort(t->heapsort(arr3), arr3);
            testSort(t->quickSort(arr4), arr4);
            testSort(t->quickSort1(arr5), arr5);
            testSort(t->mergeSort(arr6), arr6);

            System.out.println("=============================");
        }
    }

    /**
     * 调试代码用
     */
    @Test
    public void easyTest(){
        int[] no = {9,8,7,6,5,4,3,2,1};
        int[] no1 = {1,3,9,8,7,6};
//        bubble(no);
//        bubble1(no);
//        selection(no);
//        insert(no);
//        heapsort(no);
//        quickSort(no1);
//        quickSort1(no1);
//        netherlandsFlag(no1, 0, no1.length-1, 6);
        mergeSort(no);
        print(no);
    }
    /**
     * 冒泡排序
     * 相邻交换,一次外循环排好一个数
     */
    public void bubble(int[] no){
        int k = 0;
        for (int i = 0; i < no.length-1; i++) {//此处是总体遍历次数,10个元素最多遍历9次可完成排序
            for (int j = 0; j < no.length-i-1; j++) {//此处i代表已经交换完成不需要再次比较交换的元素
                if(no[j] > no[j+1]){
                    swap(no, j, j+1, k++);
                }
            }
        }
        System.out.println("冒泡排序: 数组长度为:"+no.length+";交换次数为:"+k);
//        print(no);
    }
    public void bubble1(int[] no){
        int k = 0;
        for(int i = no.length -1; i > 0; i--){
            for(int j =0; j< i; j++){
                if(no[j] > no[j+1]){
                    swap(no, j, j+1, k++);
                }
            }
        }
//        System.out.println("循环次数等差数列求和"+(1+no.length-1)*(no.length-1)/2);
        System.out.println("冒泡排序: 数组长度为:"+no.length+";交换次数为:"+k);
//        print(no);
    }

    /**
     * 选择排序
     * 每次选择一个最小的放在队首
     */
    public void selection(int[] no){
        int k = 0;
        for (int i = 0; i < no.length-1; i++) {//循环数组长度次数-1[最后一个肯定是最大,不需要再比较],每次获取列表中最小的元素放在数组队首
            int min = no[i];
            for(int j = i; j< no.length; j++){//通过比较交换,获取列表中最小元素
                if(min > no[j]){
                    k++;
                    int temp = no[j];
                    no[j] = min;
                    min = temp;
                }
            }
            no[i] = min;
        }
        System.out.println("选择排序: 数组长度为:"+no.length+";交换次数为:"+k);
//        print(no);
    }

    /**
     * 插入排序
     * 像插牌一样从后往前保证有序, 排序效率和数据状况有关系
     */
    public void insert(int[] no){
        int k = 0;
        for (int i = 1; i < no.length; i++) {//插入排序元素的起始位置,从第2个开始往前排
            for(int j=i; j>0; j--){//两两交换,保证局部有序
                if(no[j-1] > no[j]){
                    swap(no, j, j-1, k++);
                }
            }
        }
        System.out.println("插入排序: 数组长度为:"+no.length+";交换次数为:"+k);
//        print(no);
    }

    /**
     * 堆排序
     * 1.构建大根堆[利用堆的自动排序功能logn]; 2.将堆顶元素和队末元素交换[最大],输出最后一个元素
     * 数组表示堆:节点i,父节点(i-1)/2,左孩子2*i+1,右孩子2*i+2
     * 比如: 下标为0,F:0 L:1 R:2; 下标为3 F:1 L:7 R:8
     */
    public void heapsort(int[] no){
        Map<String, Integer> kmap = new HashMap<>();//计数器
        kmap.put("k",0);
        int k = 0;
        for (int i = 0; i < no.length; i++) {
            heapInsert(no, i, kmap);
        }
        int size = no.length-1;
        swap(no,0, size, k++);//获取第一个最大值[根节点放到最后]
        while(size > 0){
            heapify(no, 0, size, kmap);
            swap(no, 0, --size, k++);//将根与队末交换并"弹出"
        }
        kmap.put("k", kmap.get("k")+k);
        System.out.println("堆排序: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
//        print(no);
    }
    public void heapInsert(int[] no, int i, Map<String, Integer> kmap){
        int k = 0;
        while(no[i] > no[(i-1)/2]){ //构建大根堆:从下往上调整。和父节点比大小,比父节点大则交换
            swap(no, i, (i-1)/2, k++);
            i = (i-1)/2; //更新当前节点为父节点
        }
        kmap.put("k", kmap.get("k")+k);
    }
    public void heapify(int[] no, int i, int end, Map<String, Integer> kmap){//从上[前]往下[后]调整
        int left = 2*i+1; //左孩子节点
        int k = 0;
        while(left < end){
            int largest = left +1 < end && no[left+1] > no[left] ? left+1 : left; //选择左右孩子节点值较大的一个
            largest = no[largest] > no[i] ? largest : i;//返回孩子节点和当前节点较大的一个
            if(largest == i){//当前节点大于孩子节点,堆调整完成
                break;
            }
            swap(no, largest, i, k++);
            i = largest;//更新当前节点为孩子节点
            left = 2*i+1;
        }
        kmap.put("k", kmap.get("k")+k);
    }

    /**
     * 荷兰国旗问题
     * 给定一个数组arr和一个数num,请把小于num的数放在左边,等于num的数放在中间,大于num的数放在右边。要求额外空间为O(1),时间为O(n)
     * 思路:双指针法,夹逼[同样、其中任一指针完全遍历也可以完成该功能,双指针加速]
     * 划定小于区域和大于区域,
     * 当发现遍历的值小于num时,将遍历的值移动到小于区域[将当前值和小于区域的下一个值交换]
     * 当发现遍历的值大于num时,将遍历的值移动到大于区域[将当前值和大于区域的前一个值交换]
     *
     */
    public void netherlandsFlag(int[] no, int l, int r, int num){
        int less = l-1; //less小于区域的右边界
        int more = r+1; //more大于区域的左边界
        int k = 0;
        while(l < more){//l当前位置 当前位置不碰上右边界
            if(no[l]< num){
                swap(no, ++less, l++, k++);//当前区域和小于区域的下一个数交换,当前位置和小于区域边界右移一个位置
            }else if(no[l] > num){
                swap(no, l, --more, k++);
            }else{
                l++;
            }
        }
        print(no);
    }

    /**
     * 快速排序 - 荷兰国旗法
     * 设置基准值[队首、队尾、队中]
     * 采用双指针法,从数组两边分别比对,把小于基准值的放在左边,大于基准值的放在右边
     * 利用递归算法和分治的思想继续完成排序
     */
    public void quickSort(int[] no){
        Map<String, Integer> kmap = new HashMap<>();//计数器
        kmap.put("k",0);
        quickSort(no, 0, no.length-1, kmap);
        System.out.println("快速排序: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
//        print(no);
    }
    public void quickSort(int[] no, int l, int r, Map<String, Integer> kmap){
        int k = 0;
        if(l < r){
            swap(no, l+(int)(Math.random()*(r-l+1)), r, k++); //随机基准数
            kmap.put("k", kmap.get("k")+k);
            int[] p = partion(no, l, r, kmap);
            quickSort(no, l, p[0]-1, kmap);//小于区域
            quickSort(no, p[1]+1, r, kmap);//大于区域
        }
    }
    public int[] partion(int[] no, int l, int r, Map<String, Integer> kmap){
        int less = l-1;
        int more = r;//r作为基准值,调整完后换到中间,因此初始r即为右边界不需要r+1
        int k = 0;
        while(l < more){
            if(no[l] < no[r]){ //经典快排,当前值和最后一个值[基准值]比较
                swap(no, ++less, l++, k++);
            }else if(no[l] > no[r]){
                swap(no, --more, l, k++);
            }else{
                l++;
            }
        }
        swap(no, more, r, k++);//将基准值和大于区域左边界的值交换
        kmap.put("k", kmap.get("k")+k);
        return new int[]{less+1, more};//返回等于区域的左边界和右边界,小于区域+1为左边界,more因为刚换过,因此就是等于区域的右边界,可以使得等于区域值不参与后面的排序
    }

    /**
     * 双指针交换法,从数组两端开始遍历,交换小于基准值和大于基准值的数
     */
    public void quickSort1(int[] no){
        Map<String, Integer> kmap = new HashMap<>();//计数器
        kmap.put("k",0);
        quickSort1(no, 0, no.length-1, kmap);
        System.out.println("快速排序1: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
//        print(no);
    }
    public void quickSort1(int[] no, int l, int r, Map<String, Integer> kmap){
        if(l > r){
            return;
        }
        int k = 0;
        swap(no, l, l+(int)(Math.random()*(r-l+1)), k++); //随机基准数 队头
        int base = no[l]; //基准值
        int less = l;//左指针
        int more = r;//右指针
        while(less != more ){//while 条件不满足则停止
            while(less < more && no[more] >= base){//从右向左, 找到第一个小于基准值的数, 两个while顺序不能调换
                more--;
            }
            while(less < more && no[less] <=  base){//从左向右, 找到第一个大于基准值的数, less放下面则less作为临界值
                less++;
            }
            if(less < more){//相等时不交换
                swap(no, less, more, k++);
            }
        }
        swap(no, less, l, k++); //把小于区域边界的值和基准值交换,保证基准值在中间
        kmap.put("k", kmap.get("k")+k);
        quickSort1(no, l, less-1, kmap);
        quickSort1(no, less+1, r, kmap);
    }

    /**
     * 归并排序
     * 依然是利用递归和分治的思想,通过递归的方式将数组拆散,在合并的过程中使用辅助数组完成排序
     * 不同于其他几个排序方式,不需要交换,但需要使用O(n)的额外空间
     * 类似问题:小和
     */
    public void mergeSort(int[] no){
        if(no == null || no.length < 2){
            return;
        }
        mergeSort(no, 0, no.length-1);
        System.out.println("归并排序: 数组长度为:"+no.length+";交换次数为:0");
    }
    public void mergeSort(int[] no, int l, int r){
        if(l == r){
            return;
        }
        int mid = l + ((r-l) >> 1); //从中间将数组拆成左半边和右半边
        mergeSort(no, l, mid);
        mergeSort(no, mid+1, r);
        merge(no, l, mid, r);//从左到右,按拆分的次序合并
    }
    public void merge(int[] no, int l, int m, int r){
        int[] help = new int[r-l+1]; //辅助数组
        int i = 0; //辅助数组填值下标
        int p1 = l; //左半边指针
        int p2 = m+1; //右半边指针
        while(p1 <= m && p2 <= r){
            //左边小填左边左边指针移动,右边小填右边右边指针移动
            help[i++] = no[p1] < no[p2] ? no[p1++] : no[p2++];
        }
        //两个while只会执行一个,将左边或右边剩余的值顺序填到辅助数组里
        while(p1 <= m){
            help[i++] = no[p1++];
        }
        while(p2 <= r){
            help[i++] = no[p2++];
        }
        //将辅助数组的值回填到原数组
        for (i = 0; i < help.length; i++) {
            no[l+i] = help[i];
        }
    }


    /**
     * 交换器
     */
    public static void swap1(int [] no, int i, int j, int k){
        //引入临时元素 空间换时间
        int temp = no[j];
        no[j] = no[i];
        no[i] = temp;
    }
    public static void swap2(int[] no, int i, int j, int k){
        if(i != j){
            no[i] = no[i] + no[j];
            no[j] = no[i] - no[j];
            no[i] = no[i] - no[j];
        }
    }
    public static void swap(int[] no, int i, int j, int k){
        //两数相同异或为0
        if(i != j){
            no[i] = no[i] ^ no[j];
            no[j] = no[i] ^ no[j];//no[i] ^ no[j] ^ no[j] 此处no[j],抵消变为no[i]
            no[i] = no[i] ^ no[j];//no[i] ^ no[j] ^ no[i] 此处的no[j]相当于是原始的no[i],抵消变为no[j]
        }
    }

    /**
     * 水平打印
     */
    public static void print(int [] no){
        System.out.println(Arrays.toString(no));
    }

    /**
     * int数组对数器
     * @param sort 排序方法
     * @param arr  排序数组
     */
    public static void testSort(Consumer<int[]> sort, int[] arr){
        int[] arr2 = copyArray(arr);
        sort.accept(arr);
        Arrays.sort(arr2);
        if(!isEqual(arr, arr2)){
            System.out.println("-------------------------------------------");
            System.out.println("排序异常,数组为:");
            print(arr);
            print(arr2);
            System.out.println("-------------------------------------------");
        }
    }
    private static int[] getRandomArray(){
        int[] arr = new int[getRandomNo(maxArrayLength)];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = getRandomNo(maxNo) - getRandomNo(maxNo);//支持负数
        }
        return arr;
    }
    private static int getRandomNo(int no){
        SecureRandom random = new SecureRandom();
        int randomNo = random.nextInt(no + 1);
        return randomNo == 0?getRandomNo(no):randomNo;
    }
    private static int[] copyArray(int[] arr){
        if(arr == null){
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }
    private static boolean isEqual(int[] arr1, int[] arr2){
        if((arr1 != null && arr2 == null) || (arr1 == null && arr2 != null)){
            return false;
        }
        if(arr1 == null && arr2 == null){
            return true;
        }
        if(arr1.length != arr2.length){
            return false;
        }
        for (int i = 0; i < arr1.length; i++) {
            if(arr1[i] != arr2[i]){
                return false;
            }
        }
        return true;
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值