数据结构与算法学习笔记

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法学习笔记相关的知识,希望对你有一定的参考价值。

Gitee 项目访问地址:Data Structure And Algorithm

一、引课

  1. 几个经典的算法面试题 1

    • 子串匹配问题:KMP算法
    • 汉诺塔问题:分治算法
  2. 几个经典的算法面试题 2

    • 八皇后问题:回溯算法
    • 马踏棋盘(骑士周游问题):DFS + 贪心算法优化
  3. 内容介绍和授课模式

    • 一般来说程序会使用内存计算框架(如 Spark)和缓存技术(如 Redis)来优化程序
  4. 数据结构与算法的关系

    • 数据结构是一门研究组织数据方式的学科
  5. 编程中实际遇到的几个问题

  6. 线性结构与非线性结构

    • 常见线性结构:一维数组、栈、队列、链表

    • 常见非线性结构:二维数组、多维数组、广义表、树、图

二、稀疏数组

  1. 稀疏数组的应用场景

    • 稀疏数组(Sparse Array)的处理方法:

      1. 记录数组一共有几行几列,有多少个有效的值
      2. 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小存储的规模
  2. 稀疏数组转换的思路分析

  3. 稀疏数组的代码实现

   /**
     * 将二维数组转换为稀疏数组
     *
     * @param array 二维数组
     * @return 稀疏数组或 null
     */
   public int[][] arrayToSparse(int[][] array) 
       int rows = array.length;
       int cols = array[0].length;
   
       // 获得原数组中的有效数据个数
       int dataCount = 0;
       for (int[] rowData : array) 
           for (int colData : rowData) 
               if (colData != 0) 
                   dataCount++;
               
           
       
   
       // 创建稀疏数组,由于稀疏数组第一行存储原数组行、列、有效数个数信息,故稀疏数组行数 +1
       int[][] sparseArray = new int[++dataCount][3];
       int sparseCurrentRow = 0;
       // 稀疏数组第一行存储原数组行、列、有效数个数信息
       sparseArray[sparseCurrentRow][0] = rows;
       sparseArray[sparseCurrentRow][1] = cols;
       sparseArray[sparseCurrentRow][2] = --dataCount;
   
       // 遍历原数组,将有效数据的坐标信息及值存入稀疏数组中
       for (int i = 0; i < rows; i++) 
           for (int j = 0; j < cols; j++) 
               int data = array[i][j];
               if (data != 0) 
                   ++sparseCurrentRow;
                   sparseArray[sparseCurrentRow][0] = i;
                   sparseArray[sparseCurrentRow][1] = j;
                   sparseArray[sparseCurrentRow][2] = data;
               
           
       
       return sparseArray;
   
   
   /**
     * 将稀疏数组恢复成原二维数组
     *
     * @param sparse 稀疏数组
     * @return 二维数组 或 null
     */
   public int[][] sparseToArray(int[][] sparse) 
       if (sparse.length == 0) 
           return null;
       
       // 从稀疏数组的第一行中获取原数组的大小
       int rows = sparse[0][0];
       int cols = sparse[0][1];
   
       int[][] array = new int[rows][cols];
   
       // 从稀疏数组的第一行开始遍历,其结构为 row col val
       for (int i = 1; i < sparse.length; i++) 
           // 给需要恢复的二维数组赋值
           array[sparse[i][0]][sparse[i][1]] = sparse[i][2];
       
   
       return array;
   

三、队列

  1. 队列的应用场景和介绍

  2. 数组模拟队列的思路分析

    • 创建一个类,具有三个属性 rear(尾指针)、front(头指针)、MaxSize 以及添加数据和取出数据的方法
  3. 数组模拟队列的代码实现 1

  4. 数组模拟队列的代码实现 2

  5. 数组模拟循环队列思路分析图

    • 变量的含义做出以下调整:

      1. front:指向队列的第一个元素,初始值为 0

      2. rear:指向队尾元素的下一个元素,初始值为 0

      3. 队列满的条件:(rear + 1) % maxSize == front

      4. 队列中有效数据的个数 (rear - front + maxSize) % maxSize

  6. 数组模拟环形队列实现

    package com.bear.queue;
    
    /**
     * @author Spring-_-Bear
     * @datetime 2022/3/10 15:43
     */
    public class CircleArrayQueue 
       private final int maxSize;
       private final int[] array;
       /**
        * 队列头指针:初始化为 0
        */
       private int front;
       /**
        * 队列尾指针:初始化为 0
        */
       private int rear;
    
       public CircleArrayQueue(int maxSize) 
          this.maxSize = maxSize;
          this.array = new int[maxSize];
       
    
       public boolean isFull() 
          return (rear + 1) % maxSize == front;
       
    
       public boolean isEmpty() 
          return front == rear;
       
    
       /**
        * 添加数据到队列尾
        *
        * @param data data
        */
       public void add(int data) 
          if (isFull()) 
             System.out.println("队列已满,无法添加数据");
             return;
          
          array[rear] = data;
          rear = (rear + 1) % maxSize;
       
    
       /**
        * 获取队头元素
        *
        * @return data
        */
       public int get() 
          if (isEmpty()) 
             throw new RuntimeException("队列为空,无法取出数据");
          
          int temp = array[front];
          front = (front + 1) % maxSize;
          return temp;
       
    
       /**
        * 打印队列所有有效元素
        */
       public void list() 
          if (isEmpty()) 
             System.out.println("队列为空,无数据");
             return;
          
          for (int i = front; i < front + dataSize(); i++) 
             int index = i % maxSize;
             System.out.printf("array[%d] = %d\\n", index, array[index]);
          
       
    
       /**
        * 取得队头元素
        *
        * @return data
        */
       public int peek() 
          if (isEmpty()) 
             throw new RuntimeException("队列为空,无数据");
          
          return array[front];
       
    
       /**
        * 求出当前队列中的有效数据个数
        *
        * @return count
        */
       public int dataSize() 
          return (rear - front + maxSize) % maxSize;
       
    
    

四、单链表

  1. 单链表介绍和内存布局

  2. 单链表的创建和遍历分析实现

  3. 单链表按顺序插入节点

        /**
         * 尾插法按数据升序添加节点
         * @param head 链表头节点
         * @param node Node
         */
        public void addByOrder(Node head, Node node) 
            // 只有头节点,直接添加并返回
            if (head.next == null) 
                head.next = node;
                return;
            
    
            Node current = head;
            // 判断新加入的节点是否要添加到第一个位置,若是则直接添加到第一个位置并返回
            if (current.next.data > node.data) 
                node.next = current.next;
                current.next = node;
                return;
            
    
            current = current.next;
            // 从第一个节点开始遍历整个链表,如果当前节点不指向空且数据比新加入节点的数据小,则当前节点后移,循环寻找新节点的插入位置
            while (current.next != null && current.next.data < node.data) 
                current = current.next;
            
    
            // 将新节点指向当前节点的下一个节点
            node.next = current.next;
            // 将当前节点指向新节点
            current.next = node;
        
    
  4. 单链表节点的修改

  5. 单链表节点的删除和小结

        /**
         * 删除单链表中指定的节点
         *
         * @param head 链表头节点
         * @param node Node
         */
        public void delete(Node head, Node node) 
            Node current = head;
    
            // 找到需要删除的节点的前一个节点
            while (current.next != null && !node.data.equals(current.next.data)) 
                current = current.next;
            
    
            // 让当前节点指向当前节点的下下个节点
            current.next = current.next.next;
        
    
  6. 单链表新浪面试题

  7. 单链表腾讯面试题

    • 单链表的反转:思路 - 定义一个新的链表头节点 reverseHead,遍历原链表的同时以此将每个节点插入到 reverseHead 与 新链表上一个插入节点之间,最后让原链表的 head.next = reverseHead

          /**
           * 链表反转
           *
           * @param head 链表头节点
           */
          public void reverse(Node head) 
              // 无节点或只有一个节点,无需反转
              if (head.next == null || head.next.next == null) 
                  return;
              
              Node reverseHead = new Node(null);
              Node cur = head.next;
              Node curNext;
      
              while (cur != null) 
                  // 记录当前节点的下一个节点
                  curNext = cur.next;
                  // 将当前节点指向 reverseHead 的下一个节点即所有插入的新节点都在 reverseHead 与上一次插入的节点之间
                  cur.next = reverseHead.next;
                  // 将 reverseHead 指向当前节点
                  reverseHead.next = cur;
                  // 将记录的节点 curNext 赋值给 cur,继续遍历原链表
                  cur = curNext;
              
              head.next = reverseHead.next;
          
      
  8. 单链表百度面试题

    • 逆序打印链表:

      1. 先将原单链表反转,再遍历,破坏了原链表的结构(不推荐)
      2. 遍历原链表的每个节点并将其放入栈中,后遍历栈
    • 合并两个有序链表,使得合并后的链表依旧有序:定义一个头节点,当两个链表都不为空时获取两个链表的一个节点,比较其值后连接到头节点尾部,指针后移,当某个链表遍历完之后直接将指针指向剩下链表的那个节点

五、双向链表

  1. 双向链表的增删改查分析图解

    • 单向链表与双向链表的比较:

      1. 单向链表不能自我删除,需要借助辅助节点,而双向链表可以自我删除
  2. 双向链表的增删改查代码实现

    • 删除关键点,遍历到需要删除的节点 temp

      // 将当前节点的上一节点指向当前节点的下一节点
      temp.prev.next = temp.next;
      // 判断当前节点是否是最后一个节点
      if(temp.next != null)
          // 将当前节点的下一个节点指向当前节点的上一个节点
          temp.next.prev = temp.prev;
      
      
  3. 双向链表功能测试和小结

  4. 环形链表介绍和约瑟夫问题

  5. 约瑟夫问题分析图解和实现 1

  6. 约瑟夫问题分析图解和实现 2

    博客链接:Java环形链表解決约瑟夫(Joseph)问题

六、

  1. 栈的应用场景和介绍

    • 栈的应用场景:

      1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,回到程序调用处
      2. 处理递归调用:和子程序的调用类似,但除了存储下一个指令的地址外,还将参数、区域变量等数据存入堆栈中
      3. 表达式的转换与求值(中缀表达式转后缀表达式)
      4. 二叉树的遍历
      5. 图的深度优先(DFS)
  2. 栈的思路分析和代码实现

    • 使用数组实现栈数据结构

      package com.bear.stack;
      
      /**
       * @author Spring-_-Bear
       * @datetime 2022/3/11 9:04
       */
      public class Stack<T> 
          private Integer elementCount = 0;
          private final Object[] elementData;
          private Integer size = -1;
      
          public Stack(Integer elementCount) 
              this.elementCount = elementCount;
              this.elementData = new Object[elementCount];
          
      
          public void push(T element) 
              if (size >= elementCount) 
                  System.out.println("栈满,不能添加数据");
                  return;
              
              elementData[++size] = element;
          
      
          public Object pop() 
              if (size < 0) 
                  System.out.println("空栈,不能取出数据");
                  return null;
              
              return elementData[size--];
          
      
          public Object peek() 
              if (size < 0) 
                  System.out.println("空栈,不能取出数据");
                  return null;
              
              return elementData[size];
          
      
          public Integer size() 
              return this.size + 1;
          
      
          public void print() 
              int index = size;
              while (index >= 0) 
                  System.out.print(pop() + "\\t");
                  --index;
              
          
      
      
  3. 栈的功能测试和小结

  4. 栈实现综合计算器思路分析

    • 使用栈完成对运算表达式计算的思路分析:

      1. 定义两个栈 numStack(存放操作数)和 operatorStack(存放操作符)

      2. 使用一个 index 索引来遍历需要进行计算的运算表达式:如果是一个数字(注意需向前查看一位,以确定该数字是否是多位数),则直接压入 numStack,如果是操作符,则分以下两种情况进行:

        2.1 如果当前的符号栈为空,则直接将操作符入栈

        2.2 如果当前符号栈不为空,则比较栈顶的运算符与当前运算符的优先级,若当前操作符优先级大于栈顶的操作符优先级,则当前操作符压入符号栈;否则将栈顶的运算符弹出的同时从数栈中弹出两个数进行运算获得的运算结果 res 将其压入数栈中;再次比较当前操作符与符号栈,重复情况讨论

      3. 表达式扫描完毕,从数栈和符号栈中弹出运算符和操作数并进行运算,且将运算结果 res 压入数栈

      4. 当且仅当符号栈为空且数栈只存在一个数时,该数就是最终的计算结果

  5. 栈实现综合计算器代码实现 1

  6. 栈实现综合计算器代码实现 2

  7. 前缀 中缀 后缀表达式规则

    • 前缀表达式(波兰表达式):前缀表达式的运算符位于与其相关的操作数之前。前缀表达式的计算机求值步骤:从右至左扫描求值计算表达式(前缀表达式),遇到数字时将其压入栈顶,遇到操作符时依次弹出栈顶的两位操作数并用运算符进行计算得到结果 res,再将 res 压入栈顶;重复上述步骤直至到达计算表达式左端,最后得出的数即为运算结果
    • 后缀表达式(逆波兰表达式):后缀表达式的运算符位于与其相关的操作数之后。计算机求值步骤与前缀相似,不同的是扫描表达式的顺序是从左到右。
  8. 逆波兰计算器分析和实现 1

    • 计算后缀表达式代码实现:

          /**
           * 计算后缀表达式,要求传入的后缀表达式各操作符与操作数之间以空格间隔
           *
           * @param suffixExp 后缀表达式
           * @return 计算结果
           */
          public int calSuffixExp(String suffixExp) 
              String[] suffix = suffixExp.split(" ");
              Stack<Integer> numStack = new Stack<>();
              // 从左至右遍历后缀表达式
              for (String str : suffix) 
                  // 遇到数字则将其压入栈中
                  if (str.matches("\\\\d+")) 
                      numStack.push(Integer.valueOf(str));
                   else 
                      // 遇到操作符则从栈顶依次弹出两个操作数计算得到结果 res,并将 res 再次压入栈顶
                      numStack.push(calculate(numStack.pop(), numStack.pop(), str));
                  
              
              return numStack.pop();
          
      
  9. 逆波兰计算器分析和实现 2

  10. 中缀转后缀表达式思路分析

    • 中缀表达式转后缀表达的思路分析:

      从左到右开始扫描中缀表达式:

      1. 遇到数字,直接输出

      2. 遇到运算符:

        2.1 若为左括号 “(” 或栈为空则 直接入栈
        2.2 若为右括号 “)” 将符号栈中的元素依次出栈并输出, 直到左括号 “(“ 出栈, “(“ 只出栈不输出
        2.3 若为其他符号, 将符号栈中的元素依次出栈并输出, 直到遇到比当前符号优先级更低的符号,并将当前符号入栈

      3. 扫描完后, 将符号栈栈中剩余符号依次输出

  11. 中缀转后缀表达式代码实现 1

  12. 中缀转后缀表达式代码实现 2

        /**
         * 将中缀表达式转换为后缀表达式,生成的后缀表达式各元素之间使用一个空格间隔
         *
         * @param infixExp 中缀表达式
         * @return 后缀表达式 或 null
         */
        public String infixToSuffix(String infixExp) 
            if (infixExp == null || infixExp.length() == 0) 
                return null;
            
    
            Stack<String> operatorStack = new Stack<>();
            List<String> infixList = infixExpIntoList(infixExp);
            StringBuilder res = new StringBuilder();
            // 从左至右遍历中缀表达式
            for (String str : infixList) 
                // 如果是数字则直接输出
                if (str.matches("\\\\d+")) 
                    res.append(str).append(" ");
                 else if ("(".equals(str) || operatorStack.size() == 0) 
                    // 左括号或栈为空则直接入栈
                    operatorStack.push(str);
                 else if (")".equals(str)) 
                    // 右括号需要将符号栈中的 "(" 括号之前的操作符全部输出,"(" 只出栈不输出
                    String op;
                    while (!"(".equals((op = operatorStack.pop()))) 
                        res.append(op).append(" ");
                    
                 else 
                    // 栈顶操作符优先级大于等于当前操作符,则弹出栈顶元素,结束后将当前操作符压入栈中
                    while (operatorStack.size() > 0 && getPriority(operatorStack.peek()) - getPriority(str) >= 0) 
                        res.append(operatorStack.pop()).append(" ");
                    
                    operatorStack.push(str);
                
            
    
            // 将符号栈中剩余的操作符输出
            while (operatorStack.size() > 0) 
                res.1279 扔盘子

    数据结构与算法之深入解析汉诺塔问题的求解思路与算法示例

    手撸golang 基本数据结构与算法 快速排序

    javascript数据结构与算法——栈

    数据结构与算法笔记—— 快速排序

    算法学习——递归之快速排序