算法

时间复杂度
二进制
二进制操作

二分查找
冒泡排序
快速排序

动态规划

例子一:切钢条
例子二:过河问题
例子三:最长公共子序列
例子四:最长公共连续子序列
例子五:01背包问题

时间复杂度

一个算法在给定输入下执行的基本操作数或步数,我们假定执行每行代码需要的时间为常量

二进制

二进制原码、反码、补码:

  • 二进制的最高位是符号位:0表示正数,1表示负数
  • 正数的原码、反码、补码都一样
  • 负数的反码 = 它的原码符号位不变,其他位取反
  • 负数的补码 = 它的反码 +1
  • 0的反码、补码都是0
  • 补码的补码是原码

  • 2的原码:00000000 00000000 00000000 00000010
  • -2的原码:10000000 00000000 00000000 00000010
  • -2的反码:11111111 11111111 11111111 11111101
  • -2的补码:11111111 11111111 11111111 11111110

查看数字的二进制表示,可以用Integer.toBinaryString(-1)
十六进制其实是补码表示,0xFFFFFFFF等于十进制-1

二进制操作

"&"与
"|"或
"^"异或:不相同才为1
">>"右移:正数左边补0,负数左边补1
"<<"左移:右边补0


二分查找

描述:在有序数组里查找一个值,查找成功返回下标,否则返回-1

思路:

  1. 用数组中间的值,与给定值比较,相等则查找成功
  2. 不相等,把数组分为两半,在其中一半中继续步骤1
  3. 直到查找成功,或子数组不存在

时间复杂度:
第一步耗时O(1),一个长度为n的数组,一共要进行1 + logn次第一步,因此时间复杂度是O(logn)

    private static int binarySearch(int[] arr, int key) {
        if (arr == null || arr.length <= 0) {
            return -1;
        }
        int start = 0;
        int end = arr.length - 1;
        int mid;
        while (start <= end) {
            mid = start + (end - start) / 2; // 防止溢出
            if (arr[mid] == key) {
                return mid;
            }else if (arr[mid] > key) {
                end = mid - 1;
            }else {
                start = mid + 1;
            }
        }
        return -1;
    }

冒泡排序

思路:

  1. 针对无序序列,两两比较,顺序不对则交换,一趟排序后序列末尾的值是有序的
  2. 重复步骤1,直到无序序列为空
    private static void bubbleSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        int end = arr.length - 1;
        int tem;
        for (int i = end - 1; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                if (arr[j] > arr[j+1]) {
                    tem = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tem;
                }
            }
        }
    }

时间复杂度:
上述算法,无论输入如何,时间复杂度都是O(N^2)

冒泡排序改进:
上述代码,如果内层循环中没有进行数据交换,那么内层循环中的值已经是有序的了,此时可以跳出循环

    private static void bubbleSort1(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        int end = arr.length - 1;
        int tem;
        boolean swapped;
        for (int i = end - 1; i >= 0; i--) {
            swapped = false;
            for (int j = 0; j <= i; j++) {
                if (arr[j] > arr[j+1]) {
                    tem = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tem;
                    swapped = true;
                }
            }
            if (!swapped)
                break;
        }
    }

时间复杂度:
此时如果输入已经是正序的,那么外层循环只执行一次,此时时间复杂度O(n)
因此对冒泡排序改进来说,最好的时间复杂度是O(n)


快速排序

思路:

  1. 选取一个主元,进行一次定位,定位后,主元左边元素都比它小,主元右边元素都比它大
  2. 针对左右两个分区,分别执行步骤1
    // 定位
    private static int position(int[] arr, int start, int end) {
        int i = start;
        int j = end;
        int key = arr[i];

        while (i < j) {
            while(i < j && arr[j] > key) { // 由于可能出现i==j,因此每一步都加i<j判断,防止ij乱加减
                j--;
            }
            if (i < j) {
                arr[i++] = arr[j];
            }
            while (i < j && arr[i] <= key) {
                i++;
            }
            if (i < j) {
                arr[j--] = arr[i];
            }
        } // 最后i==j
        arr[i] = key; // 由于是覆盖,而不是交换,因此最后要有一步赋值
        return i;
    }

    // 快速排序
    private static void quickSort(int[] arr, int start, int end) {
        if (arr == null || arr.length <= 1 || start >= end || start < 0 || end > arr.length) {
            return;
        }

        int pos = position(arr, start, end);
        quickSort(arr, pos+1, end);
        quickSort(arr, start, pos-1);
    }

快速排序还有很多改进版本,如随机选择基准数,区间内数据较少时直接用另的方法排序以减小递归深度。也可以选中间的数作为基准数,要实现这个方便非常方便,直接将中间的数和第一个数进行交换就可以了(我们用的是覆盖,因此这里是用第一个数覆盖中间的数)

        int key = arr[i];
        替换为
        int mid = start + (end - start) / 2;
        int key = arr[mid];
        arr[mid] = arr[i];

时间复杂度:
最好场景:T(n) = 2T(n / 2) + n,最好时间复杂度O(nlogn)
最坏场景:T(n) = T(n - 1) + n,最坏时间复杂度O(n^2)


动态规划

动态规划和分治算法相似,都是通过组合子问题的解来求解原问题。
特殊情况下,分治算法会反复求解子问题
动态规划每个子问题只求解一次,将其保存起来,避免不必要的计算
动态规划通常用来求解最优化问题。
动态规划有下面两种方式,我们通常使用第二种

  • 带备忘的自顶向下法:通常用一个数组保存已经计算过的值,求解时,如果数组中有值,直接返回,否则才进行计算。(计算f(n),在计算f(n)的过程中去计算f(n-1)...)
  • 自底向上法:求解一个问题时,直至它依赖的所有子问题均已求解完成,才求解它。(先计算f(0),在计算f(1)...直到f(n))
例子一:切钢条

问题描述:长度为i的钢条,价格为p(i),i是整数。给定一段长度为n的钢条,怎么切能让收益最大?
思路:

  • 长度为n的钢条,最大切割收益用f(n)表示。
  • f(n)怎么计算呢,考虑钢条n所有可能的切割方案,先想第一步,第一刀切在哪,第一刀可以切长度为1,2,3,...,n(这是所有可能的场景),那么第一刀切长度i的收益是p(i)+f(n-i)
  • 最大切割收益f(n),即所有可能场景的最大值,于是f(n) = max(p(i)+f(n-i)), i=1 to n
    直接递归法
    private static int cut(int[] price, int n) {
        if (n == 0) {
            return 0;
        }
        int res = -1;
        for (int i = 1; i <= n; i++) {
            System.out.println(n + "-" + (n-i));
            res = Math.max(res, price[i-1] + cut(price, n-i));
        }
        return res;
    }

动态规划-带备忘自顶向下法

    private static int cut1(int[] price, int n) {
        int[] fArr = new int[n+1];
        for (int i = 0; i < n+1; i++) {
            fArr[i] = -1;
        }
        return cut1_aug(price, n, fArr);
    }

    private static int cut1_aug(int[] price, int n, int[] fArr) {
        if(fArr[n] >= 0) {
            return fArr[n];
        }

        int res = -1;
        if (n == 0) {
            res = 0;
        }else {
            for (int i = 1; i <= n; i++) {
                System.out.println(n + "-" + (n-i));
                res = Math.max(res, price[i-1] + cut1_aug(price, n-i, fArr));
            }
        }
        fArr[n] = res;
        return res;
    }

动态规划-自底向上法

    private static int cut2(int[] price, int n) {
        int[] f = new int[n+1]; // 最优解数组
        for (int i = 1; i <= n; i++) {
            int res = -1;
            for (int j = 1; j <= i; j++) {
                System.out.println("*");
                res = Math.max(res, price[j-1] + f[i-j]);
            }
            f[i] = res;
        }
        return f[n];
    }
例子二:过河问题

一座桥,n个人,一个手电筒。桥一次最多通过两个人,两个人过桥时间为两人中时间较长者,过桥必须用手电筒,所以每次过桥之后需要有人把手电筒送回来,问n个人过桥总时间最短是多少?

http://www.360doc.com/content/08/0706/02/37063_1402145.shtml
A ---> B
最佳场景:
1.每次B->A送手电筒的人,一定是B当中最快的
2.手电筒在A时,速度最快的人一定在A
3.每次A->B的两人,要么这两人其中一个是所有人中最快的,要么这两个人到B之后再也不回来
4.每次B->A送手电筒的人,一定是所有人中最快的,或者次快的

n个人中:
-最快的人,单人过桥时间设为a
-次快的人,单人过桥时间设为b
-次慢的人,单人过桥时间设为y
-最慢的人,单人过桥时间设为z

那么,最慢和次慢的人过桥有两种模式
模式一:(耗时y+a+z+a)

  • ay过桥
  • a回来
  • az过桥
  • a回来

模式二:(耗时b+a+z+b)

  • ab过桥
  • a回来
  • yz过桥
  • b回来

另一种思路,同样按过河时间从小到大排序

  1. 1个人:a直接过
  2. 2个人:ab直接过
  3. 3个人:ab-a-ac
  4. 4个人:和上面的模式相同
    场景一:ab-a-ac-a-ad,耗时b+a+c+a+d
    场景二:ab-a-cd-b-ab,耗时b+a+d+b+b
  5. n个人
    因为一次最多过两个人,所以考虑最后升两个人(耗时最长),这两个人的过河方式有2种,对应上面两种模式
    模式一:f(n) = f(n-2) + a + c + a + d
    模式二:f(n) = f(n-2) + a + d + b + b

第i个人过河时间为a[i],递增
于是 f(n) = min{f(n-2)+arr[1]+arr[n-1]+arr[1]+arr[n], f(n-2)+arr[1]+arr[n]+arr[2]+arr[2]}

    /**
     * 过河时间
     * 输入:每个人单独过河时间,从小到大排列
     * 输出:所有人过河最短时间
     */
    private static int leastTime(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;
        }

        int i = arr.length - 1;
        int sum = 0;
        for (; i > 2; i -= 2) {
            int t1 = 2 * arr[0] + arr[i-1] + arr[i];
            int t2 = 2 * arr[1] + arr[0] + arr[i];
            sum += Math.min(t1, t2);
        }

        if (i == 2) { // 剩下3人
            sum = sum + arr[0] + arr[1] + arr[2];
        }

        if (i == 1) { // 剩下2人
            sum = sum + arr[1];
        }
        return sum;
    }
例子三:最长公共子序列LCS

描述:给定两个序列X、Y,如果Z即是X的子序列,又是Y的子序列,那么称Z是X和Y的公共子序列
比如:X=abcbdab,Y=bdcaba,那么bcba是X和Y的一个最长公共子序列

思路:
我们设X={x1,x2...xm},Y={y1,y2...yn}两个序列,Z={z1,z2...zk}是X和Y的任意一个LCS,那么
1.如果xm = yn,那么zk=xm=yn且Zk-1是Xm-1和Yn-1的一个LCS
2.如果xm != yn,那么zk != xm表示Z是Xm-1和Y的一个LCS
3.如果xm != yn,那么zk != yn表示Z是X和Yn-1的一个LCS

于是
用c[i,j]表示Xi和Yj的LCS长度,可以得到下面的公式


DX-20190606@2x.png
    // 最长公共子序列
    private static int lcsLenth(char[] x, char[] y) {
        if (x.length == 0 || y.length == 0) {
            return 0;
        }

        int m = x.length;
        int n = y.length;
        int[][] res = new int[m+1][n+1]; // 动态规划典型用法,字典
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j<= n; j++) {
                if (x[i-1] == y[j-1]) {
                    res[i][j] = res[i-1][j-1] + 1;
                }else if (res[i-1][j] > res[i][j-1]) {
                    res[i][j] = res[i-1][j];
                }else {
                    res[i][j] = res[i][j-1];
                }
            }
        }
        return res[m][n];
    }
例子四:最长公共子串

https://blog.csdn.net/u010397369/article/details/38979077
描述:给定两个字符串X和Y,求最长公共子串,注意子串是连续了(上面说的子序列可以不连续)
思路:和求最长公共子序列相同的思路
我们定义S(i,j)表示“以xi和yj结尾的最长公共子串”,用c[i,j]表示其长度
那么

DX-20190606@2x.png

    // 最长公共子串
    private static int lcsLenth2(char[] x, char[] y) {
        if (x.length == 0 || y.length == 0) {
            return 0;
        }

        int m = x.length;
        int n = y.length;
        int[][] res = new int[m+1][n+1];
        int max = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (x[i-1] == y[j-1]) {
                    res[i][j] = res[i-1][j-1] + 1;
                    if (res[i][j] > max) {
                        max = res[i][j];
                    }
                }
            }
        }
        return max;
    }

上述方法需要一个mn的数组,空间复杂度可以优化到O(1)
上述方法实际是计算了下面这个数组


image.png

我们可以遍历每一条对角线,求出最大值,这样只需要O(1)的空间

    // 最长公共子串
    private static int lcsLenth3(char[] x, char[] y) {
        if (x.length == 0 || y.length == 0) {
            return 0;
        }

        int i = x.length + y.length - 1;
        int tem = 0; // 代替上面二维数组的临时变量
        int max = 0;
        while (i > 0) { // 从矩形右上角,沿上边和左边,遍历到左下角
            int col = i > y.length ? (i-y.length) : 0; // 对角线起点 列
            int row = i > y.length ? 0 : (y.length - i); // 对角线起点 行
            while (col < x.length && row < y.length) {
                if (x[col] == y[row]) {
                    tem++;
                    max = tem > max ? tem : max;
                }else {
                    tem = 0;
                }
                col++;
                row++;
            }
            i--;
        }
        return max;
    }
例子五:01背包问题

描述:有N件物品和一个容量为V的背包。第i件物品的容量是c(i),价值是v(i)。每种物品仅有一件,可以选择放或不放。求解将哪些物品装入背包可使价值总和最大。
思路:
f[i][j] = max{f[i-1][j], f[i-1][j-c(i)] + v(i)}
f[i][j]表示:“将前i件物品放入容量为j的背包中”的最大价值

考虑第i件物品,
1.背包单独放不下,即j<c(i)
-- 此时f[i][j]=f(i-1,j)
2.背包单独放的下,即j>=c(i)
-- 放,此时最大价值是,把前i-1个物品放入容量为j-c(i)的背包的最大价值,加上物品i的价值
-- 不放,此时最大价值是,把前i-1个物品放入容量为v的背包,f[i-1][j]
f[i][j] = max{f[i-1][j], f[i-1][j-c(i)] + v(i)}

    // 01背包
    private static int maxV(int[] c, int[] v, int pac) {
        if (c.length == 0 || v.length == 0 || c.length != v.length) {
            return -0;
        }

        int num = c.length; // 物品个数
        int[][] maxV = new int[num+1][pac+1]; // 行号:物品号 列号:容量大小

        for (int i = 1; i <= num; i++) {
            for (int j = 1; j <= pac; j++) {
                if (j < c[i-1]) {
                    maxV[i][j] = maxV[i-1][j];
                }else if (maxV[i-1][j] > maxV[i-1][j-c[i-1]] + v[i-1]) {
                    maxV[i][j] = maxV[i-1][j];
                }else {
                    maxV[i][j] = maxV[i-1][j-c[i-1]] + v[i-1];
                }
            }
        }
        return maxV[num][pac];
    }

    public static void main(String[] args) {
        int[] c = {4,5,6,2,2}; // 物品占用
        int[] v = {6,4,5,3,6}; // 物品价值
        maxV(c, v, 10);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容