zoukankan      html  css  js  c++  java
  • 读书笔记-算法

    几个对数组的算法

    1, 找出数组中的最大值:

    1
    2
    3
    4
    5
    
    double max = a[0];
    
    for(int i = 1; i < a.length; i++)
    
        if(a[i] > max) max = a[i];
    

    //把最大值马上设定为数组的第一个元素,然后遍历数组,如果有别当前这个最大值更大的元素,则把最大值更新,直到遍历结束;

    2, 计算数组的平均值:

    1
    2
    3
    4
    5
    6
    7
    
    double sum = 0.0;
    
    for(int i = 0; i < a.length; i++)
    
        sum += a[i];
    
    double average = sum / a.length;
    

    //算出总值,然后除以数组的元素数;

    3,复制数组:

    1
    2
    3
    4
    5
    6
    7
    
    double[] b = new double[a.length];
    
    for(int i = 0; i < a.length; i++) 
    
        b[i] = a[i];
    
    //new 一个和原数组同length同类型的数组,然后遍历赋值每个元素;
    

    4,颠倒数组元素的顺序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    double [] b = new double[a.length];
    
    for(int i = a.length - 1, j = 0; i > -1 && j < b.length ; i--, j++) 
    
        b[j] = a[i];
    
    //这是个直观低效率算法,时间消耗(a.length),空间消耗(2 * a.length),并且有两个循环指数i, j;
    
    for(int i =0; i < a.length / 2; i++) {
    
        double temp = a[i];
    
        a[i] = a[a.length - 1 - i];
    
        a[a.length - 1 - i] = temp;
    }
    

    偶数个元素的交换过程:

    < 2 1, 2, 3, 4

    0 4, 2, 3, 1

    1 4, 3, 2, 1

    奇数个元素的交换过程:

    < 2 1, 2, 3, 4, 5

    0 5, 2, 3, 4, 1

    1 5, 4, 3, 2, 1

    //可见,无论是偶数个还是奇数个元素,交换的次数都一样,都是a.length/2,偶数个的时候是全交换,奇数个的时候,是以中间的那个元素为中心点,其他元素都交换,中间元素并不在a.length/2的遍历范围内;这个算法,时间(a.length/2),空间(a.length);

    5,a[][] * b[][] = c[][], 矩阵相乘(方阵):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    int n = a.length;
    
    double[][] c = new double[n][n];
    
    for(int i = 0; i < n; i++)
    
        for (int j = 0; j < n; j++) {
    
            for(int k = 0; k < n; k++) //计算行i和列j的点乘
    
                c[i][j] += a[i][k] * b[k][j];
    
        }
    

    典型静态方法的实现

    1,计算整数的绝对值:

    1
    2
    3
    4
    5
    
    public static int abs(int x) {
        if(x < 0)    return -x;
    
        else    return x;
    }
    

    //绝对值的规则很简单:不小于零就是本身,反之就返回-x;

    2, 计算浮点数的绝对值:

    1
    2
    3
    4
    5
    
    public static double abs(double x) {
        if( x < 0.0)    return -x;
    
        else    return x;
    }
    

    3, 判断一个数是否是素数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public static boolean isPrime(int n) {
        if(n < 2)    return false;                        //大于1的自然数,1不是素数
    
        for(int i = 2; i * i <= n; i++) {              //i * i <=n
    
            if(n % i == 0)    return false;
    
        return true;
    }
    

    //素数,就是质数。指在一个大于1的自然数中,除了1和此整数自身外,不能被其他自然数整除的数。

    判断的关键点:

    a, 小于2,不是;

    b,从2到n遍历,遍历到一个i i > n之前的数就提前结束遍历,因为2到满足i i <= n的i之间的这些数如果能整除n,那么i之后到n的这些数也能;以满足i * i <=n为分界线,1…n之间的数对于整除n来说,是对称的;

    判断n能否被i整除:n % i 的值是否为0;

    4, 计算平方根(牛顿迭代法):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    public static double sqrt(double c) {
    
        if(c < 0) return Double.NaN;
    
        double err =1e-15;                 //1乘以10的负15次方
    
        double t = c;
    
        while(Math.abs(t - c / t) > err *t)
    
            t = (c/t + t) / 2.0;
        return t;
    }
    

    //不懂, TODO

    5, 计算直角三角形的斜边:

    1
    2
    3
    4
    
    public static double hypotenuse(double a, double b) {
    
        return Math.sqrt(a * a + b * b);
    }
    

    6, 计算调和级数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    public static double H(int n) {
    
        double sum = 0.0;
    
        for(int i = 1; i <=n; i++) 
    
            sum += 1.0 / i;
    
        return sum;
    }
    

    //形如1/1+1/2+1/3+…+1/n+…的级数称为调和级数,它是 p=1 的p级数。 调和级数是发散级数。在n趋于无穷时其部分和没有极限(或部分和为无穷大)。


    二分查找的递归和循坏实现法

    递归总有一个最简单的情况-方法的第一条语句总是包含return的条件语句;

    递归调用总是去尝试解决一个规模更小的子问题,这样递归才能收敛到最简单的情况;

    递归调用的父问题和尝试解决的子问题之间不应该有交集;

    二分查找:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public static int rank(int key, int[] a) {
    
        return rank(key, a, 0, a.length - 1);
    
    }
    
    public static int rank(int key, int[] a, int lo, int hi) {
    
        if(lo > hi)
    
            return -1;
    
        int mid = lo + (hi - lo) / 2;   //数组并没有被拆分,所以这里(hi - lo)/2必须再加上lo
    
        if(key < a[mid])     return rank(key, a, lo, mid - 1);
    
        else if(key > a[mid]     return rank(key, a, mid +1, hi);
    
        else     return mid;
    }
    

    //如果原始的方法参数不怎么适合递归或者不够递归方法,就另写一个满足要求的递归方法,用原始的调用之;

    比如二分查找的时候,并没有给定递归时需要的低坐标和高坐标,如果坚持要在原始方法中使用递归,那么必须对数组进行拆分和复制,效率低下,浪费空间;

    在不拆分数组的情况下,“父问题和子问题之间不应该有交集“, 所以mid不是简单的lo + hi /2了;mid不用再被传给子问题,因为== mid的话就是解了已经,低位传lo到mid-1;高位传mid + 1到hi;


    循环实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    int lo = 0;
    
    int hi = a.length - 1;
    
    while(lo <= hi) {
    
        int mid = lo + (hi - lo) / 2; //不拆分数组的话,始终是这个公式
    
        if(key < a[mi])     hi = mid - 1;
    
        if(key > a[mi])     lo = mid + 1;
    
        else     return mid;
    }
    
    return -1;
    

    不用temp实现swap

    1
    2
    3
    4
    5
    
    a = a + b;
    
    b = a - b;
    
    a = a - b;
    

    Dijkstra双栈算术表达式求值法

    双栈:一个操作数栈,一个操作符栈;

    从左到有遍历算数表达式:

    1, 忽略左括号;

    2, 将数字push入操作数栈;

    3, 将运算符push入操作符栈;

    4, 遇到有括号时,pop一个运算符,pop出所需数量的操作数,并将运算符和操作数的运算结果push入操作数栈。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
    public static double calc(String[] equation) {
    
        Stack<String> ops = new Stack<String>();
    
        Stack<Double> vals = new Stack<Double>();
    
        for(String s : equation) {
    
            if(s.equals("("))
    
                ;
    
            else if(s.equals("+"))
    
                ops.push(s);
    
            else if(s.equals("-"))
    
                ops.push(s);
    
            else if(s.equals("*"))
    
                ops.push(s);
    
            else if(s.equals("/"))
    
                ops.push(s);
    
            else if(s.equals("sqrt"))
    
                ops.push(s);
    
            else if(s.equals(")")) {
    
                String op = ops.pop();
    
                double v = vals.pop();
    
                if(op.equals("+")) 
    
                    v = vals.pop() + v;
    
                if(op.equals("-")) 
    
                    v = vals.pop() - v;
    
                if(op.equals("*")) 
    
                    v = vals.pop() * v;
    
                if(op.equals("/")) 
    
                    v = vals.pop() / v;
    
                if(op.equals("sqrt")) 
    
                    v = Math.sqrt(v);
    
                vals.push(v);
    
            } else
    
                vals.push(Double.parseDouble(s));
    
        }
    
        return vals.pop();
    }
    

    这其实就是编译原理中的解释器。

    应该也有其他方法,比如全部push入栈之后再依次出栈,前提是优先级用括号来明示。


    堆栈的数组和链表实现以及队列的链表实现

    堆栈的意义:

    堆栈并不是为了迭代的一个容器,虽然它是一个可迭代的容器,但它不应该被应用于静态的数据存储场景;

    堆栈应该应用于动态的运算、过滤等场景;

    创建泛型类型的数组作为数据存储:

    直接创建泛型数组是不可以的:T[] items = new T[2];

    只能通过这种方式创建:T[] items = (T[])(new Object[2]);

    动态增减数组大小:

    动态增减的数组实现:没有高深的方法,就是new一个新的数组,把值拷贝过来,再把引用赋予新的数组对象:

    1
    2
    3
    4
    5
    6
    7
    
    T[] temp = (T[]) new Object[newLength];
    
    for(int i = 0; i < N; i++)     //如果是减小数组,这里的条件需要修改
    
        temp[i] = a[i];
    
    a = temp;
    

    增:当数组满的时候,直接增加1倍;

    减:当数组不满1/4的时候,减少至1/2;

    以上是基于内存开销和性能之间的平衡,尤其是缩减数组,不能一个一个减,也不能不满1/2的时候直接减掉1/2,这样数组马上又满了,可能又需要增。

    Stack内部使用动态增减的数组后,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    public void push(T item) {
    
        if(N == a.length)
    
            resize(2 * a.length)
    
        a[N++] = item;
    
    }
    
    public T pop() {
    
        T item = a[--N];
    
        a[N] = null;  
    
        if(N > 0 && N == a.length / 4) //如果堆栈只满1/4,减为1/2,还能有1/2的空余;
    
            resize(a.length / 2);
    
        return item;
    }
    

    pop()方法中要避免内存泄漏:

    对象游离: Stack的pop方法写的不好,就有可能导致内存泄漏;

    用数组实现堆栈,pop之后,当前对象在堆栈范围内已经无用了,如果客户代码那也用完了这个对象,其该被回收,但是,因为Stack内部的数组还有对这个对象的引用,导致无法被GC,除非再次push,该数组位的引用值被重新指向另一个对象,原来那个对象就被GC了;

    如果用API中的List实现堆栈,因为List本身的remove方法已经采取了避免对象游离的措施,所以就没这个问题;

    用数组存储和用链表存储:

    堆栈本身并不用于遍历,所以操作(push和pop)的用时都跟集合大小无关,不管是用数组还是用链表实现;

    用数组存储的明显缺点就在于,push,pop会不定期地引起数组的调整,调整数组的耗时和栈大小成正比,克服这个缺陷就是用链表代替之:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    private Node first = null; //栈顶节点
    
    private Class Node { //描述栈帧的节点内部类定义
    
        T item;
    
        Node next;
    
    }
    
    public void push(T item) {
    
        Node oldFirst = first;
    
        first = new Node();
    
        first.item = item;
    
        first.next = oldFirst;
    
        N++;
    
    }
    
    public T pop() {
    
        T item = first.item;
    
        first = first.next();
    
        N--;
    
        return item;
    }
    

    不用担心对象游离的问题,first = first.next之后,由于堆栈里并没有数组结构,出栈的对象在栈内不会再被引用,没用就回收掉了;

    堆栈也是集合,也要实现迭代器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    Stack<T> implements Iterable<T>
    
    public Iterator<T> iterator() {
    
        return new Iterator<T> {
    
            private int i = N;
    
            public boolean hasNext() {
    
                return i > 0;
    
            }
    
            public T next() {
    
                return a[--i];
    
            }
    
            ...
    
        }
    

    链表实现的迭代:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    Stack<T> implements Iterable<T>
    
    public Iterator<T> iterator() {
    
        return new Iterator<T> {
    
            private Node current = first;
    
            public boolean hasNext() {
    
                return current != null;
    
            }
    
            public T next() {
    
                 T item = current.item;
    
                current = current.next;
    
                return item;
    
            }
    
            ...
    
        }
    

    链表实现的队列就是一种堆栈,仍然从first出队,但是从last入队:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    public boolean isEmpty() {
    
        return first === null;
    
    }
    
    public void enqueue(T item) {
    
        Node oldLast = last;
    
        last = new Node();
    
        last.item = item;
    
        last.next = null;
    
        if(isEmpty())
    
            first = last;
    
        else
    
            oldLast.next = last;
    
        N++;
    
    }
    
    public Item dequeue() {
    
        T item = first.item;
    
        first = first.next;
    
        if(isEmpty())
    
            last = null;
    
        N--;
    
        return item;
    }
    

    到底用数组还是链表:

    堆栈是LIFO,数组实现的话,总是在数组的末尾进行赋值和置null,这个可以接受;但数组的调整大小问题导致数组实现又不是特别能接受,而链表就完全不存在这个问题;

    队列是FIFO,数组实现的话,假如入队末尾,那么出队必然在开头,删除开头元素要引起数组整体挪动;或者反过来,入队在开头,则出队在末尾,入队得在开头添加元素,引起数组整体挪动;所以说,队列一点都不适合用数组实现。


    常数、对数、线性、线性对数、平方、立方、指数;

    一般来说,平方、立方、指数级别的算法对于大规模的问题是不可用的;

    logN的底数对算法分析来说相当于一个常数,所以可以忽略底数到底是几;

    2-sum问题的平方级算法:

    1
    2
    3
    4
    5
    6
    7
    
    for(int i =0; i < N; i++) 
    
        for(int j = i + 1; j < N; j++) 
    
            if(a[i] + a[j] == 0)
    
                count++;
    

    这个复杂度是(N-1) + (N-2) + … + (N-N) = N的平方 + N的平方/2 =-= N的平方;

    改进的算法:

    1
    2
    3
    4
    5
    6
    7
    
    sort(a);
    
    for(int i = 0; i < N; i++) 
    
        if(BinarySearch(-a[i], a) > i)
    
            count++;
    

    这个算法的思路,单循环数组a,对于每一个a[i]:

    1, 如果二分查找找不到-a[i],计数器不增加;

    2, 如果二分查找到的-a[i]是a[j],如果j > i,计数器增加,反之如果j < i,因为a是排序了的,说明这次查找之前已经用a[j]找到过a[i]了,重复了,计算器不增加;

    对于N次单循环,每次都二分查找了,二分查找的复杂度是对数级,所以这个算法的总复杂度是线性对数级;

    相应的,原来为N的立方的3-sum问题可被优化为N的平方对数级的;


    优先队列的二叉堆实现

    【优先队列】

    堆栈:删除最新元素;

    队列:删除最旧元素;

    优先队列:删除最大元素和插入元素;

    优先队列实现的两个方式:

    1, 惰性的,使用无序数据结构,插入元素不做任何操作,删除最大元素时再查找最大元素;

    2, 主动的,使用有序数据结构,插入元素时就排到合适的位置,删除最大元素时直接删第一个;

    优先队列的初级实现:

                            插入元素                        删除最大元素
    

    有序数组 N 1

    无序数组 1 N

    栈和队列的操作的复杂度都是个常数;

    优先队列用数组初级实现的话,操作的复杂度都是线性的;

    我们试图探寻更好性能的优先队列实现;

    【二叉堆】

    用数组表示完全二叉树:

    将二叉树的节点按照层级顺序放入数组中,根节点在位置1,它的子节点在位置2和3,而子节点的子节点则分别在4、5、6、7;

    不使用数组的第一个位置;

    对于一个节点,它在数组中的是索引是k,那么它的父节点的索引是:下取整(k/2)

    子节点的索引分别是2k和2k+1;

    堆有序:

    当一棵二叉树的每个节点都大于等于它的两个子节点时;

    在堆有序的二叉树中,从任意节点向上,都能得到一列非递减的元素;从任意节点向下,都能得到一列非递增的元素;

    堆有序化-上浮:

    如果堆的有序状态因为某个节点变得比它的父节点更大而打破,就需要交换它和它的父节点;

    交换后,这个节点仍然可能比现在的父节点大,所以需要继续往上交换;

    1
    2
    3
    4
    5
    6
    7
    8
    
    void swim(int k) {
    
        while(k > 1 && pq[k/2] < pq[k]) {
    
            swap(pq, k/2, k);
    
            k = k / 2;
    }
    

    同理,下沉:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    void sink(int k) {
    
        while(2 * k <= N) {
            int j = 2 * k;
    
            if(j < N && pq[j] < pq[j+1]) //选取两个子节点中较大的一个往上交换
    
                    j++;
    
            if(pq[k] >= pq[j])   //结束下沉,已经比字节点大了
    
                    break;
    
            swap(pq, k, j); //下沉
    
            k = j;
    }
    

    在实现二叉堆的数组中,插入一个数据到末尾是上浮;删除第一个数据,这个数据就是最大元素,然后把数组最末尾的元素放到顶端,让其下沉;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    public class MaxPQ<Key extends Comparable<Key>> {
    
        private Key[] pq;
    
        private int N = 0;
    
        public MaxPQ(int maxN) {
    
            pq = (key[]) new Comparable[maxN+1];
    
        }
    
        public boolean isEmpty() {
    
            return N == 0;
    
        }
    
        public int size() {
    
            return N;
    
        }
    
        public void insert(Key v) {
    
            pq[++N] = v;
    
            swim(N);
    
        }
    
        public Key delMax() {
    
            Key max = pq[1];
    
            swap(1, N--);
    
            pq[N+1] = null;
    
            sink(1);
    
            return max;
    
        }
    }
    

    由于插入和删除都最多是一次根节点到叶节点的堆秩序恢复,跟节点到叶节点的最长路径是lgN;所以二叉堆实现的优先队列的插入和删除的复杂度都是lgN(比较次数:lgN+1, 2lgN);


    如何检测一个链表中是不是有循环结点?

    两个指针,分别表示乌龟和兔子,乌龟每次往下走1步,兔子每次走两步,如果兔子走到了Next为Null的节点,说明没有循环,否则他们一定相遇,表示有循环。

  • 相关阅读:
    linux sysfs (2)
    微软——助您启动云的力量网络虚拟盛会
    Windows Azure入门教学系列 全面更新啦!
    与Advanced Telemetry创始人兼 CTO, Tom Naylor的访谈
    Windows Azure AppFabric概述
    Windows Azure Extra Small Instances Public Beta版本发布
    DataMarket 一月内容更新
    和Steve, Wade 一起学习如何使用Windows Azure Startup Tasks
    现实世界的Windows Azure:与eCraft的 Nicklas Andersson(CTO),Peter Löfgren(项目经理)以及Jörgen Westerling(CCO)的访谈
    正确使用Windows Azure 中的VM Role
  • 原文地址:https://www.cnblogs.com/mosthink/p/5288682.html
Copyright © 2011-2022 走看看