数据结构与算法

目录

  • 数据结构与算法
  • 1.代码效率优化方法论
  • 1.1 衡量程序的运行效率 - 复杂度
  • 1.2 数据结构
  • 数据结构基础
  • 2.1 数据处理的基本操作 (增删查)
  • 2.2 线性表
  • 2.3 栈
  • 2.4 队列
  • 2.5 数组
  • 2.6 字符串

1.代码效率优化方法论

1.1 衡量程序的运行效率 - 复杂度

代码消耗的资源一般分为两个维度,计算时间和计算空间,也就是时间复杂度和空间复杂度。一般更加关注资源消耗与输入数据量之间的关系而不是具体的资源消耗量。

  1. 复杂度分为:时间复杂度、空间复杂度。
  2. 时间复杂度的计算和代码的结构,代码的执行次数有关系。
  3. 空间复杂度的计算和数据结构,存储资源有关系。
  4. 复杂度和常系数无关如:O(n)、O(2n)、O(n/2)都表示O(n)复杂度
  5. 多个复杂度相加,取高次项复杂度为最终结果,如:O(n^3 + n^2 +n) 那么这个复杂度就是O(n^3)
  6. 常见的一些时间复杂度有:
  • 对一个数组遍历循环,若数组长度为n,那么时间复杂度为O(n)。
  • 嵌套遍历,若外层遍历n次,内层遍历m次,则时间复杂度为O(m*n)。
  • 二分查找,或者更通用地说是采用分而治之的二分策略,时间复杂度都是 O(logn)。
  • 若两个遍历不是嵌套的,还是顺序的,那么时间复杂度依然是O(n)+O(n) = O(2n) = O(n),最终依然为O(n)。
  • 若n和程序执行的次数无关,那么时间复杂度始终为O(1);比如针对一个长度为n的字符串直接进行输出操作,不管长度为多长,始终只需输出一次。
  1. 优化时间复杂度很重要,处理10万的数据,程序结构定下的不同的时间复杂度,让计算机计算的次数是有天壤之别的。
1.2 数据结构
  1. 时间昂贵,空间廉价
  2. 暴力解法,即假定不限制时间和空间,例如对指定范围内的每个数字都做一次判断
  3. 程序优化降低复杂度,通常通过梳理程序查找流程中的无效计算和存储。通常降低时间复杂度:递归,二分法,排序算法,动态规划等
    降低空间复杂度:降低数据结构复杂度
  4. 时间转换为空间,设计合理数据结构,完成时间复杂度向空间复杂度的转移

数据结构基础

2.1 数据处理的基本操作 (增删查)
  • 找到要处理的数据。这就是按照某些条件进行查找
  • 把结果存到一个新的内存空间中。这就是在现有数据上进行新增
  • 把结果存到一个已使用的内存空间中。这需要先删除内存空间中的已有数据,再新增新的数据。

常用分析方法:

  • 这段代码对数据进行了哪些操作?
  • 这些操作中,哪个操作最影响效率,对时间复杂度的损耗最大?
  • 哪种数据结构最能帮助你提高数据操作的使用效率?

增和删:

可以细分为在数据结构中间的增和删,以及在数据结构最后的增和删

查找:

按照位置条件的查找和按照数据数值特征的查找

2.2 线性表

线性表是 n 个数据元素的有限序列,最常用的是链式表达,通常也叫作线性链表或者链表。在链表中存储的数据元素也叫作结点,一个结点存储的就是一条数据记录。每个结点的结构包括数据值和指向下一节点的指针

线性表案例:

  • 链表的翻转。给定一个链表,输出翻转后的链表。例如,输入1 ->2 -> 3 -> 4 ->5,输出 5 -> 4 -> 3 -> 2 -> 1
while(curr){
    next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
}
  • 给定一个奇数个元素的链表,查找出这个链表中间位置的结点的数值
while(fast && fast.next && fast.next.next){
    fast = fast.next.next;
    slow = slow.next;
}
  • 判断链表是否有环

如果链表存在环,快指针和慢指针一定会在环内相遇,即 fast == slow 的情况一定会发生

python如何用standford corenlp计算依存距离和依存方向_字符串

2.3 栈
  1. 后进先出
  2. 表尾用来输入数据,通常也叫作栈顶(top);相应地,表头就是栈底(bottom)
  3. 顺序栈:栈的顺序存储可以借助数组来实现。一般来说,会把数组的首元素存在栈底,最后一个元素放在栈顶。然后定义一个 top 指针来指示栈顶元素在数组中的位置
  4. 链栈:
  • 需要增加指向栈顶的 top 指针,这是压栈和出栈操作的重要支持
  • 栈顶放在单链表的头部

例1给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须与相同类型的右括号匹配,左括号必须以正确的顺序匹配。例如,{ [ ( ) ( ) ] } 是合法的,而 { ( [ ) ] } 是非法的

private static boolean isLeft(char c){
        if (c == '{' || c == '[' || c == '('){
            return true;
        }else {
            return false;
        }
    }

    private static boolean isRight(char c){
        if (c == '}' || c == ']' || c == ')'){
            return true;
        }else {
            return false;
        }
    }

    private static boolean isPair(char c1, char c2) {
        if ((c1 == '{' && c2 == '}') || (c1 == '[' && c2 == ']') || (c1 == '(' && c2 == ')')){
            return true;
        }else {
            return false;
        }
    }

    private static String isLegal(String s){
        Stack<Character> stack = new Stack<Character>();
        for (int i = 0; i < s.length(); i++) {
                char curr = s.charAt(i);
                if (isLeft(curr)){
                    stack.push(curr);
                }else {
                    if (stack.empty()){
                        return "非法";
                    }

                    char p = stack.pop();
                    if (!isPair(p,curr)){
                        return "非法";
                    }
                }

        }
        if (stack.empty()){
            return "合法";
        }else {
            return "非法";
        }
    }

    public static void main(String[] args) {
        String s = "{[()()]}";
        System.out.println(isLegal(s));
    }

例2 浏览器的页面访问都包含了后退和前进功能,利用栈实现

  • 两个栈,分别用来支持后退和前进
  • 当用户访问了一个新的页面,则对后退栈进行压栈操作
  • 当用户后退了一个页面,则后退栈进行出栈,同时前进栈执行压栈
  • 当用户前进了一个页面,则前进栈出栈,同时后退栈压栈

python如何用standford corenlp计算依存距离和依存方向_字符串_02

例3 链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6,k = 3,则打印 321654

Stack<Integer> stack = new Stack<Integer>();
        for (int i = 1; i <= 8; i++) {
            stack.push(i);
        }

        int k =4;
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();

        for (int i = 0; i < 4; i++) {
            stack1.push(stack.pop());
        }

        for (int i = 0; i < 4; i++) {
            stack2.push(stack.pop());
        }

        for (int i = 0; i < 4; i++) {
            stack.push(stack1.pop());
        }

        for (int i = 0; i < 4; i++) {
            stack2.push(stack.pop());
        }

        for (Integer integer : stack2) {
            System.out.println(integer);
        }
2.4 队列
  1. 先进先出
  • 先进,表示队列的数据新增操作只能在末端进行
  • 先出,队列的数据删除操作只能在始端进行
  1. 顺序队列
  • 数据在内存中也是顺序存储
  • 新增数据的操作,就是利用 rear 指针在队尾新增一个数据元素,时间复杂度为 O(1)
  • 不惜消耗 O(n) 的时间复杂度去移动数据 - 解决数组越界
  • 或者开辟足够大的内存空间确保数组不会越界 - 解决数组越界
  1. 链式队列
  • 依赖链表来实现,其中的数据依赖每个结点的指针互联,在内存中并不是顺序存储
  • 单链表,增加了 front 指针和 rear 指针
  • 为了防止删除最后一个有效数据结点后, front 指针和 rear 指针变成野指针,加入头结点
  1. front 指针删除数据的操作引发了时间复杂度过高,移动指针的方式来删除数据可能产生数组越界的问题
  2. 确定队列长度最大值,建议使用循环队列。 无法确定队列长度时,考虑使用链式队列

练习: 约瑟夫环,已知 n 个人(以编号 1,2,3...n 分别表示)围坐在一张圆桌周围。从编号为 k 的人开始报数,数到 m 的那个人出列;他的下一个人又从 1 开始报数,数到 m 的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。这个问题的输入变量就是 n 和 m,即 n 个人和数到 m 的出列的人。输出的结果,就是 n 个人出列的顺序。

private static void ring(int num1, int num2){
        LinkedList<Integer> linkedList = new LinkedList<>();

        for (int i = 1; i <= num1; i++) {
            linkedList.add(i);
        }

        int k = 2;
        int element = 0;
        int count = 0;
        for (;count < k; count++){
            element = linkedList.poll();
            linkedList.add(element);
        }
        //模拟从第K个人开始数

        count = 1;
        while (linkedList.size()>0) {
            if (count < num2){
                element = linkedList.poll();
                linkedList.add(element);
                count++;
            }else {
                element = linkedList.poll();
                count = 1;
                System.out.println(element);
            }
        }
        //当循环队列长度小于预设m值时循环数


    }

    public static void main(String[] args) {
        ring(5,3);
    }
2.5 数组
  1. 数组的增删查操作
  • 增加:若插入数据在最后,则时间复杂度为 O(1);如果中间某处插入数据,则时间复杂度为 O(n)
  • 若删除数据在最后,则时间复杂度为 O(1),对应位置的删除,扫描全数组,时间复杂度为 O(n)
  • 如果只需根据索引值进行一次查找,时间复杂度是 O(1),查找一个数值满足指定条件的数据,则时间复杂度是 O(n)

练习:给定数组 nums = [1,1,2],函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2。 又如,给定 nums = [0,0,1,1,1,2,2,3,3,4],函数应该返回新的长度 5,并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4

int[] nums = {0,0,1,1,1,2,2,3,3,4};

    int temp = nums[0];
    int len = 1;

        for (int i = 1; i < nums.length; i++) {
        if (temp != nums[i]){
            temp = nums[i];
            nums[len] = temp;
            len ++;
        }
    }

        System.out.println(len);
        for (int i = 0; i < (nums.length-len); i++) {
        System.out.println(nums[i]);
    }
2.6 字符串
  1. 特殊字符串
  • 空串,指含有零个字符的串。例如,s = "",书面中也可以直接用 Ø 表示
  • 空格串,只包含空格的串。它和空串是不一样的,空格串中是有内容的,只不过包含的是空格,且空格串中可以包含多个空格。例如,s = " ",就是包含了 3 个空格的字符串
  • 子串,串中任意连续字符组成的字符串叫作该串的子串
  • 原串通常也称为主串
  1. 字符串的顺序存储结构,是用一组地址连续的存储单元来存储串中的字符序列,一般是用定长数组来实现。有些语言会在串值后面加一个不计入串长度的结束标记符,比如 \0 来表示串值的终结
  2. 字符串的链式存储结构,与线性表是相似的,但由于串结构的特殊性(结构中的每个元素数据都是一个字符),如果也简单地将每个链结点存储为一个字符,就会造成很大的空间浪费。因此,一个结点可以考虑存放多个字符,如果最后一个结点未被占满时,可以使用 "#" 或其他非串值字符补全
  3. 链式存储中,每个结点设置字符数量的多少,与串的长度、可以占用的存储空间以及程序实现的功能相关
  • 如果字符串中包含的数据量很大,但是可用的存储空间有限,那么就需要提高空间利用率,相应地减少结点数量
  • 而如果程序中需要大量地插入或者删除数据,如果每个节点包含的字符过多,操作字符就会变得很麻烦,为实现功能增加了障碍
  1. 字符串查找
  • 假设要从主串 s = "goodgoogle" 中找到 t = "google" 子串
//1.判断字符串s2是否是字符串s1的模式串
        String s1 = "goodgoogle";
        String s2 = "google";

        boolean flag = false;
        for (int i = 0; i < s1.length(); i++) {
            if (s1.charAt(i) == s2.charAt(0)){

                if (s2.length() == 1) {
                    flag = true;
                    break;
                }

                int k = i;
                for (int j = 1; j < s2.length(); j++) {
                    k++;
                    if (s1.charAt(k) != s2.charAt(j)){
                        break;
                    }
                    if (j == (s2.length()-1)){
                        flag = true;
                    }
                }

            }
        }

        if (flag){
            System.out.println("包含");
        } else {
            System.out.println("不包含");
        }

可优化的地方:

第一个for循环可以减少循环的次数,少循环子串的长度次,因为当主串所剩长度小于子串时就没有必要继续循环了

for (int i = 0; i < s1.length()-s2.length()+1; i++) {}

对于第二个for循环可以让j从0开始增加,然后让s1.charAt(i+j)和s2.charAt(j)比较即可,这样还可以省去第一步判断该字串是否长度仅为1

for (int j = 1; j < s2.length(); j++) {
                   
                    if (s1.charAt(i+j) != s2.charAt(j)){
                        break;
                    }
  • 查找出两个字符串的最大公共字串
    使用contains的情况下
String s1 = "1234567";
        String s2 = "1111567";
        
        outer: for (int i = s2.length(); i > 0; i--) {
            for (int j = 0; j <= (s2.length()-i); j++) {
                if (s1.contains(s2.substring(j,j+i))){
                    System.out.println(s2.substring(j,j+i));
                    break outer;
                }
            }
        }
  • 给定一个字符串,逐个翻转字符串中的每个单词。例如,输入: "the sky is blue",输出: "blue is sky the"
String s1 = "the sky is blue";
        StringBuffer result = new StringBuffer();
        Stack<Character> stack = new Stack<>();

        for (int i = s1.length()-1; i >= 0; i--) {
            if (s1.charAt(i) != ' '){
                stack.push(s1.charAt(i));
            }

            if (s1.charAt(i) == ' ' || i == 0){
                while (!stack.empty()){
                    result.append(stack.pop());
                }

                if (i != 0){
                    result.append(' ');
                }
            }
        }

        System.out.println(result);