数据结构与算法整理

文章目录

    • 时间&空间复杂度
    • 数据结构
      • 线性与非线性
      • 常见数据结构
    • 常见排序算法
      • 交换排序·冒泡排序
      • 交换排序·快速排序
      • 选择排序
      • 堆排序
      • 插入排序
      • 归并排序
      • 基数排序
      • Comparator接口
    • 常见查找算法
      • 二分查找
      • 插值查找
    • 树结构
      • 二叉树
      • 二叉排序树
      • 红黑树
      • B树
      • B+树
      • 赫夫曼树
    • 递归与分治
    • 动态规划
    • 贪心算法
    • KMP算法

时间&空间复杂度

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

常见的时间复杂度有:常数阶O(1), 对数[logarithmic]阶O(log2n), 线性阶O(n), 线性对数阶O(nlog2n), 平方阶O(n2), 立方阶O(n3) ,k次方阶O(nk), 指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
算法时间复杂度由小到大依次为:Ο(1) < Ο(log2n)[二分] < Ο(n) < Ο(nlog2n) < Ο(n2) < Ο(n3) < … < Ο(2n) < Ο(n!)

求解算法的时间复杂度的具体步骤是:

  • 找出算法中的基本语句;
    算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
  • 计算基本语句的执行次数的数量级;
    只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
  • 用大O记号表示算法的时间性能。
    将基本语句执行次数的数量级放入大Ο记号中。

空间复杂度比较常用的有:O(1)O(n)O(n²)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
如果新建一个数组,这个数据占用的大小为n,虽然有循环,但没有再分配新的空间,因此,空间复杂度主要看数组大小即可,即 S(n) = O(n)

数据结构

线性与非线性

线性:数组(Array)、链表(LinkedList)、栈(Stack)、队列(Queue)
非线形:多维数组、树结构、图结构

常见数据结构

  • 稀疏数组
    场景:解压缩
    当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存。
    处理方法是:
    第一行记录数组一共有几行几列和不同值的个数;而后记录每行不同值的位置与实际值

  • 环形队列
    通过取模的方式实现。尾索引的下一个为头索引时表示队列满

  • 双向链表
    prev,next

  • 单向环形链表
    (约瑟夫环)
    先创建一个节点,让first指向自己,形成环形;后面每创建一个新节点,就将其加入到链表中(first.next=new;new.next=first)。


  • 先进后出结构,入栈push(top++),出栈pop(top–) [poll取空不报错]

常见排序算法

  • 插入排序
    • 直接插入排序
    • 希尔排序
  • 选择排序
    • 简单选择排序
    • 堆排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 归并排序
  • 基数排序

内排序:所有排序操作都在内存完成
外排序:由于数据太大,需要把数据放在磁盘,通过磁盘和内存的数据传输才能进行

交换排序·冒泡排序

重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。

public void maopao() {
    int[] arr = {0, -1, 4, -2, 9, 5};
    int temp;
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 - i; j++) { //趟数是len-1-i
            if (arr[j + 1] < arr[j]) {
                temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    System.out.println(Arrays.toString(arr));
}

交换排序·快速排序

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

public void quickSort() {
    int[] arr = {0, -1, 4, -2, 9, 5, -3, 4, 8, 7};
    sort(arr, 0, arr.length - 1);
    System.out.println(Arrays.toString(arr));
}

private void sort(int[] arr, int left, int right) {
    int l = left; //左下标
    int r = right; //右下标
    int pivot = arr[(left + right) / 2]; //中轴值
    int temp = 0;

    while (l < r) {
        while (arr[l] < pivot) { //在pivot左边找
            l += 1;
        }
        while (arr[r] > pivot) { //在pivot右边找
            r -= 1;
        }
        if (l >= r) {
            break;
        }

        //交换
        temp = arr[l];
        arr[l] = arr[r];
        arr[r] = temp;

        if (arr[l] == pivot) { //前移
            r -= 1;
        }
        if (arr[r] == pivot) { //后移
            l += 1;
        }
    }

    if (l == r) {
        l += 1;
        r -= 1;
    }
    if (left < r) { //向左递归
        sort(arr, left, r);
    }
    if (right > l) { //向右递归
        sort(arr, l, right);
    }
}

选择排序

首先在未排序的序列中找到最小(大)元素,存放到排序序列的起始位置;再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

public void chooseSort() {
    int[] arr = {0, -1, 4, -2, 9, 5};
    int mini, temp;
    for (int i = 0; i < arr.length - 1; i++) {
        mini = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[mini]) { //找到最小(或最大)
                mini = j;
            }
        }
        temp = arr[i];
        arr[i] = arr[mini];
        arr[mini] = temp;
    }
    System.out.println(Arrays.toString(arr));
}

堆排序

也是一种 选择排序 ,也是一种 完全二叉树 :每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;小于等于称为小顶堆。

插入排序

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

public void insertSort() {
    int[] arr = {0, -1, 4, -2, 9, 5};
    int lasti, curr;
    for (int i = 1; i < arr.length; i++) {
        lasti = i - 1; //上一个索引
        curr = arr[i];
        while (lasti >= 0 && arr[lasti] > curr) {
            arr[lasti + 1] = arr[lasti];
            lasti--;
        }
        arr[lasti + 1] = curr;
    }
    System.out.println(Arrays.toString(arr));
}

其他:希尔排序,缩小增量排序

归并排序

将多个有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。

public void mergeSortTest() {
    int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
    int[] temp = new int[arr.length]; //额外空间排序
    mergeSort(arr, 0, arr.length - 1, temp);
    System.out.println(Arrays.toString(arr));
}

private void mergeSort(int[] arr, int left, int right, int[] temp) {
    if (left < right) {
        int middle = (left + right) / 2;
        mergeSort(arr, left, middle, temp); //向左递归分解
        mergeSort(arr, middle + 1, right, temp); //向右递归分解
        merge(arr, left, middle, right, temp); //再合并
    }
}

public void merge(int[] arr, int left, int middle, int right, int[] temp) {
    int i = left; //左边初始序列索引
    int j = middle + 1; //右边初始序列索引
    int t = 0; //指向temp数组的当前索引

    while (i <= middle && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[t] = arr[i];
            t += 1;
            i += 1;
        } else {
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }
    }

    //剩余元素填充
    while (i <= middle) {
        temp[t] = arr[i];
        t += 1;
        i += 1;
    }
    while (j <= right) {
        temp[t] = arr[j];
        t += 1;
        j += 1;
    }

    //将temp数组拷贝到arr
    t = 0;
    int tempLeft = left;
    while (tempLeft <= right) {
        arr[tempLeft] = temp[t];
        t += 1;
        tempLeft += 1;
    }
}

基数排序

桶排序一种,将整数按照位数切割,然后按每个位数分别比较(消耗存储,容易OOM)

Comparator接口

案例一 · 文件修改时间排序

class CompratorByLastModified implements Comparator<File> {
    public int compare(File f1, File f2) {
        long diff = f2.lastModified() - f1.lastModified();
        if (diff > 0)
            return 1;
        else if (diff == 0)
            return 0;
        else
            return -1;
    }

    public boolean equals(Object obj) {
        return true;
    }
}

案例二 · 文件名称排序

class CompratorByName implements Comparator<File> {
    public int compare(File f1, File f2) {
        if (f1.isDirectory() && f2.isFile())
            return -1;
        if (f1.isFile() && f2.isDirectory())
            return 1;
        return f2.getName().compareTo(f1.getName());
    }

    public boolean equals(Object obj) {
        return true;
    }
}

Arrays.sort(fs, new CompratorByLastModified());

常见查找算法

二分查找

分治算法的一种。二分搜索必须保证数据 有序

public static void main(String[] args) {
    int[] arr = new int[]{1, 4, 5, 7, 8, 12, 45, 67, 99, 105};
    System.out.println(biSearch(arr, 99));
}

public static int biSearch(int[] arr, int num) {
    int start = 0;
    int end = arr.length - 1;
    int mid;
    while (start <= end) {
        System.out.println("*");
        mid = (start + end) / 2;
        if (arr[mid] == num) {
            return mid + 1;
        } else if (arr[mid] > num) { // 向左查找
            end = mid - 1;
        } else { // 向右查找
            start = mid + 1;
        }
    }
    return -1;
}

public static int biSearch2(int[] arr, int start, int end, int num) {
    if (start > end) {
        return -1;
    }
    int mid = (start + end) / 2;
    int midVal = arr[mid];
    if (midVal == num) {
        return mid;
    } else if (midVal > num) { // 向左查找
        return biSearch2(arr, start, end - 1, num);
    } else { // 向右查找
        return biSearch2(arr, start + 1, end, num);
    }
}

// 查找多个值的情况

插值查找

类似于二分,不同的是每次从自适应mid处开始查找,适用于连续且量大的数据
公式: int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left])
注意需要判断findVal过大或过小越界的情况

树结构

数组、链表和二叉树的优缺点略

二叉树

每个节点最有只有两个子节点(左节点右节点
满二叉树、完全二叉树

前序遍历:先输出父节点,再遍历左子树和右子树
中序遍历:先遍历左子树,再输出父节点,再遍历右子树
后序遍历:先遍历左子树,再遍历右子树,再输出父节点

二叉排序树

左边比根节点小,右边比根节点大
二叉排序树既可以保证数据的检索速度,同时也可以保证增删改的速度
问题:极端情况,插入有序会退化成链表

public void insert(Tree tree, int value) {
    if (tree.getValue() == 0) {
        tree.setValue(value);
    } else if (tree.getValue() < value) {
        if (tree.getRight() == null) {
            tree.setRight(new Tree());
        }
        insert(tree.getRight(), value);
    } else if (tree.getValue() > value) {
        if (tree.getLeft() == null) {
            tree.setLeft(new Tree());
        }
        insert(tree.getLeft(), value);
    }
}

红黑树

平衡二叉树AVL的一种,降低树的高度,树高最大最小之差不超过1,实现方式:旋转(左、右、双旋转(左+右))。Java中的TreeSet底层用的就是红黑树。
左旋转:

Node newNode = new Node(value);
newNode.left = left;
newNode.right = right.left;
value = right.value;
right = right.right;
left = newNode;

B树

多路搜索树,每个节点可以拥有多个孩子节点,进一步降低树的高度
一般用于文件系统的索引

B+树

在B树的基础上改造,它的数据都在叶子节点,同时叶子节点之间还加了指针形成链表
由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了
一般用于数据库索引(如果只取一条数据,Hash快)

赫夫曼树

赫(哈Huffman)夫曼树 带权路径长度最小的二叉树,也称最优二叉树

创建一个赫夫曼树

public static void main(String[] args) {
    int[] arr = {10, 5, 8, 12, 9, 4, 14, 20};
    Node huffman = create(arr);
    // 遍历验证
    huffman.print();
}

public static Node create(int[] arr) {
    List<Node> nodes = new ArrayList<>();
    for (int val : arr) {
        nodes.add(new Node(val));
    }
    while (nodes.size() > 1) {
        Collections.sort(nodes); //排序(从小到大)
        Node leftNode = nodes.get(0); //取出最小
        Node rightNode = nodes.get(1); //取出第二小

        Node parent = new Node(leftNode.value + rightNode.value); //构建新的树
        parent.left = leftNode;
        parent.right = rightNode;

        // 删除处理过的节点
        nodes.remove(leftNode);
        nodes.remove(rightNode);
        nodes.add(parent);
    }
    return nodes.get(0);
}
static class Node implements Comparable<Node> {
    int value;
    Node left;
    Node right;
    public Node(int value) {
        this.value = value;
    }

    public void print() {
        System.out.println(this.value);
        if (this.left != null) {
            this.left.print();
        }
        if (this.right != null) {
            this.right.print();
        }
    }
    @Override
    public int compareTo(Node node) {
        return this.value - node.value;
    }
    @Override
    public String toString() {
        return "Node{" + "value=" + value + '}';
    }
}

应用:通讯领域信息传输、文件压缩解压,赫夫曼编码(按照各个字符出现的次数进行编码)

递归与分治

递归
当程序执行到一个方法时,就会独立开辟一个空间(栈),每个空间中的局部变量也是独立的。
注意递归调用处前后代码的执行顺序

public static void show(int n) {
    System.out.println("n1: " + n); // 10 -> 2
    if (n > 2) {
        show(n - 1);
    }
    System.out.println("n2: " + n); // 2 -> 10
}

分治
待解决复杂的问题能够简化为几个若干个小规模相同的问题,然后逐步划分,达到易于解决的程度。应用:阶乘、斐波纳契数列、汉诺塔问题、棋盘覆盖、找出伪币、求最值

案例一 · 汉诺塔

public static void main() {
    move(5, "A柱", "B柱", "C柱");
}

public static void move(int num, String a, String b, String c) {
    if (num == 1) {
        disPaly(num, a, c);
    } else {
        move(num - 1, a, c, b);
        disPaly(num, a, c);
        move(num - 1, b, a, c);
    }
}

public static void disPaly(int num, String s1, String s2) {
    System.out.println("第" + num + "个塔从" + s1 + "到" + s2);
}     

案例二 · 走迷宫

public static void main(String[] args) {
    int[][] map = {
            {1, 1, 1, 1, 1, 1, 1, 1, 1},
            {1, 0, 0, 0, 0, 0, 0, 0, 1},
            {1, 1, 1, 1, 0, 1, 1, 1, 1},
            {1, 1, 0, 0, 0, 0, 0, 1, 1},
            {1, 0, 0, 1, 0, 1, 1, 1, 1},
            {1, 0, 1, 1, 0, 1, 1, 1, 1},
            {1, 0, 0, 1, 1, 1, 1, 1, 1},
            {1, 0, 0, 0, 1, 1, 1, 1, 1},
            {1, 1, 1, 0, 1, 1, 1, 1, 1},
            {1, 1, 1, 0, 0, 0, 0, 0, 1},
            {1, 1, 1, 1, 1, 1, 1, 1, 1}
    };

    System.out.println("原来的地图 11 * 9");
    for (int i = 0; i < 11; i++) {
        for (int j = 0; j < 9; j++) {
            System.out.print(map[i][j] + " ");
        }
        System.out.println();
    }

    // 1,1是起点 9,7是终点
    setWay(map, 1, 1);
    System.out.println("走过的地图");
    for (int i = 0; i < 11; i++) {
        for (int j = 0; j < 9; j++) {
            if (map[i][j] == 2) {
                System.out.format("\33[32;1m" + map[i][j] + "\33[0m ");
            } else {
                System.out.print(map[i][j] + " ");
            }
        }
        System.out.println();
    }
}

// 0-还没走 1-不可以走 2-可以走 3-走过不行

public static boolean setWay(int[][] map, int x, int y) {
    if (map[9][7] == 2) {
        return true;
    } else {
        if (map[x][y] == 0) {
            map[x][y] = 2;
            if (setWay(map, x + 1, y)) { //下
                return true;
            } else if (setWay(map, x, y + 1)) { //右
                return true;
            } else if (setWay(map, x - 1, y)) { //上
                return true;
            } else if (setWay(map, x, y - 1)) { //左
                return true;
            } else {
                map[x][y] = 3;
                return false;
            }
        } else {
            return false;
        }
    }
}

动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解,使用填表的方式。
与分治法不同的是,分解得到的子问题是非独立的,即下一个子阶段的求解是建立在上一个子阶段的解的基础上。

案例一 · 有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。

public static int[] steps = new int[11];
public static void main() {
    steps[10] = calStep(10);
    for (int i = 0; i < steps.length; i++) {
        System.out.print(steps[i] + " ");
    }
    System.out.println();
    System.out.println(steps[10]);
}

private static int calStep(int n) {
    if (n == 1 || n == 2) {
        return n;
    }

    if (steps[n - 1] == 0) {
        steps[n - 1] = calStep(n - 1);
    }

    if (steps[n - 2] == 0) {
        steps[n - 2] = calStep(n - 2);
    }
    return steps[n - 1] + steps[n - 2];
}

案例二 · 给定一个矩阵,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和

public void step() {
    int[][] arr = {
            {4, 1, 5, 3},
            {3, 2, 7, 7},
            {6, 5, 2, 8},
            {8, 9, 4, 5}
    };
    int result = minSteps(arr);
    System.out.println("result=" + result);
}

private static int minSteps(int[][] arr) {
    int row = arr.length;
    int col = arr[0].length;
    int[][] steps = new int[row][col];
    steps[0][0] = arr[0][0];
    for (int i = 1; i < row; i++) { //列+,从上到下
        steps[i][0] = steps[i - 1][0] + arr[i][0];
    }
    for (int j = 1; j < col; j++) { //行+,从左到右
        steps[0][j] = steps[0][j - 1] + arr[0][j];
    }

    for (int i = 1; i < row; i++) {
        for (int j = 1; j < col; j++) {
            steps[i][j] = Math.min(steps[i - 1][j], steps[i][j - 1]) + arr[i][j];
        }
    }
    /*for (int i = 0; i < steps.length; i++) {
        for (int i1 = 0; i1 < steps.length; i1++) {
            System.out.print(steps[i][i1] + " ");
        }
        System.out.print("\n");
    }*/
    return steps[row - 1][col - 1];
}

//优化解法
public static int minSteps2(int[][] arr) {
    int col = arr[0].length;
    int[] steps = new int[col];
    steps[0] = arr[0][0];
    for (int i = 1; i < col; i++) {
        steps[i] = steps[i - 1] + arr[0][i];//第一行
    }

    for (int i = 1; i < arr.length; i++) {
        steps[0] = arr[i][0] + steps[0];//每一行的最左边
        for (int j = 1; j < col; j++) {
            steps[j] = Math.min(steps[j - 1] + arr[i][j], steps[j] + arr[i][j]);
        }
    }

    System.out.println(Arrays.toString(steps));
    return steps[col - 1];
}

贪心算法

在每一步求解的步骤中,它要求“贪婪”的选择最佳操作,并希望通过一系列的最优选择,能够产生一个问题的(全局的)最优解(不一定是最优,但会接近)。

案例一 · 有若干面额纸币,问得到总金额n需要最少的面额张数

private static int remain = 512;

public static void main(String[] args) {
    Map<Integer, Integer> map = new TreeMap<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    map.put(1, 3);
    map.put(2, 1);
    map.put(10, 1);
    map.put(5, 2);
    map.put(50, 3);
    map.put(20, 2);
    map.put(100, 5);
    System.out.println(map);

    Map<Integer, Integer> result = tanxin(map);
    result.forEach((k, v) -> {
        if (v != 0) {
            System.out.println("需要面额为" + k + "的共" + v + "张");
        }
    });
}

public static Map<Integer, Integer> tanxin(Map<Integer, Integer> map) {
    Map<Integer, Integer> result = new TreeMap<>();
    map.forEach((k, v) -> {
        int c = Math.min(remain / k, v);
        remain = remain - c * k;
        result.put(k, c);
    });
    return result;
}

KMP算法

KMP是一个解决某子串在文本串是否出现过的算法,简称字符串查找算法。
获取一个字符串(子串)的部分匹配值:如ABCDA前缀(A、AB、ABC、ABCD),后缀(BCDA、CDA、DA、A),此时匹配值为[0, 0, 0, 0, 1]

public void kmp() {
    String str1 = "ABC ABBDDA ADABCDACDCB";
    String str2 = "ABCDA";

    int[] match = kmpMatch(str2);
    System.out.println(Arrays.toString(match));

    int index = kmpSearch(str1, str2, match);
    System.out.println("index=" + index);
}

public static int kmpSearch(String str1, String str2, int[] match) {
    for (int i = 0, j = 0; i < str1.length(); i++) {
        while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
            // KMP算法核心
            j = match[j - 1];
        }

        if (str1.charAt(i) == str2.charAt(j)) {
            j++;
        }
        if (j == str2.length()) {
            return i - j + 1;
        }
    }
    return -1;
}

// 获得子串的部分匹配表
public static int[] kmpMatch(String dest) {
    int[] match = new int[dest.length()];
    match[0] = 0;
    for (int i = 1, j = 0; i < dest.length(); i++) {
        while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
            j = match[j - 1];
        }
        if (dest.charAt(i) == dest.charAt(j)) {
            j++;
        }
        match[i] = j;
    }
    return match;
}

扩展:图(DFS-深度优先、BFS-广度优先)
算法:普里姆(Prim)算法
克鲁斯卡尔(Kruskal)算法
迪杰斯特拉(Dijkstra)算法
弗洛伊德(Floyd)算法

你可能感兴趣的:(Java)