算法与数据结构—学习总结


title: 慕课网-算法与数据结构—学习总结
date: 2020-08-12 16:21:41
tags: LearnNotes

慕课网-算法与数据结构-学习总结


第一章 引言

课程源码 play-with-Algorithms

第二章 排序基础

  • 排序的稳定性
    排序后是否改变原序列键值相同的序列的先后关系
  • 内排序与外排序
    外排序: 由于排序记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行 。外部排序最常用的算法是多路归并排序
  • 影响排序的三个方面
    • 时间性能
      • 比较
      • 移动
    • 辅助空间
    • 算法复杂度

交换排序

  • 冒泡排序
    • 基本思想:两两交换,将最大(或最小)的交换至队列前

选择排序

  • 简单选择排序

    • 基本思想:在未排序的序列种找到最小(或最大)的元素放到前面
    • 在这里插入图片描述
  • 参考实现代码(cpp)

        template<typename T>
        void selectionSort(T arr[], int n){
    
            for(int i = 0 ; i < n ; i ++){
    
                int minIndex = i;
                for( int j = i + 1 ; j < n ; j ++ )
                    if( arr[j] < arr[minIndex] )
                        minIndex = j;
    
                swap( arr[i] , arr[minIndex] );
            }
        }
    
  • 直接选择排序

  • 树型选择排序

插入排序及其改进

  • 直接插入排序
    • 基本思想: 将未排序的元素插入到已经排好序的队列种对应的位置
      在这里插入图片描述
    • 实现代码
      template<typename T>
      void insertionSort(T arr[], int n){
          for( int i = 1 ; i < n ; i ++ ) {
      
          // 寻找元素arr[i]合适的插入位置
          // 写法1
          //        for( int j = i ; j > 0 ; j-- )
          //            if( arr[j] < arr[j-1] )
          //                swap( arr[j] , arr[j-1] );
          //            else
          //                break;
      
          // 写法2
          //        for( int j = i ; j > 0 && arr[j] < arr[j-1] ; j -- )
          //            swap( arr[j] , arr[j-1] );
      
          // 写法3
                      T e = arr[i];
                      int j; // j保存元素e应该插入的位置
                      for (j = i; j > 0 && arr[j-1] > e; j--)
                          arr[j] = arr[j-1];
                      arr[j] = e;
          }
          return;
      }
      
    • 改进:
      • 折半插入排序: 找到已排好序种对应位置时 用 折半查找
      • 希尔排序:
        • 基本思想:交换不相邻的元素以对数组的局部进行排序
          在这里插入图片描述
        • 第一个O(n log n)的排序算法
        • 参考代码
          template<typename T>
          void shellSort(T arr[], int n){
          
              // 计算 increment sequence: 1, 4, 13, 40, 121, 364, 1093...
              int h = 1;
              while( h < n/3 )
                  h = 3 * h + 1;
          
              while( h >= 1 ){
          
                  // h-sort the array
                  for( int i = h ; i < n ; i ++ ){
          
                      // 对 arr[i], arr[i-h], arr[i-2*h], arr[i-3*h]... 使用插入排序
                      T e = arr[i];
                      int j;
                      for( j = i ; j >= h && e < arr[j-h] ; j -= h )
                          arr[j] = arr[j-h];
                      arr[j] = e;
                  }
          
                  h /= 3;
              }
          }
          
        • 希尔排序及其优化
      • 其他: 路插入排序,表插入排序等。

第三章 高级排序问题

归并排序及其优化

  • 基本思想: (分治)将排序序列差分成 两个等长的子序列,对子序列进行排序后再归并
  • 参考核心代码(cpp)
        // 使用优化的归并排序算法, 对arr[l...r]的范围进行排序
        template<typename T>
        void __mergeSort2(T arr[], int l, int r){
    
            // 优化2: 对于小规模数组, 使用插入排序
            if( r - l <= 15 ){
                insertionSort(arr, l, r);
                return;
            }
    
            int mid = (l+r)/2;
            __mergeSort2(arr, l, mid);
            __mergeSort2(arr, mid+1, r);
    
            // 优化1: 对于arr[mid] <= arr[mid+1]的情况,不进行merge
            // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
            if( arr[mid] > arr[mid+1] )
                __merge(arr, l, mid, r);
        }
    
  • 优化点:
    - 当排序的元素少于一定(如16)时,直接调用 插入排序
    - 如果第一个序列的最大值小于第二个序列的最小值,则不用比较,直接合并
  • 归并排序的自底向上写法(迭代)
    • 核心代码参考
              // 使用自底向上的归并排序算法
              template <typename T>
              void mergeSortBU(T arr[], int n){
                  // Merge Sort Bottom Up 优化
                  // 对于小数组, 使用插入排序优化
                  for( int i = 0 ; i < n ; i += 16 )
                      insertionSort(arr,i,min(i+15,n-1));
    
                  for( int sz = 16; sz < n ; sz += sz )
                      for( int i = 0 ; i < n - sz ; i += sz+sz )
                          // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
                          if( arr[i+sz-1] > arr[i+sz] )
                              __merge(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
                  // Merge Sort BU 也是一个O(nlogn)复杂度的算法,虽然只使用两重for循环
                  // 所以,Merge Sort BU也可以在1秒之内轻松处理100万数量级的数据
                  // 注意:不要轻易根据循环层数来判断算法的复杂度,Merge Sort BU就是一个反例
                  // 关于这部分陷阱,推荐看(liuyubobo老师)的《玩转算法面试》课程,第二章:《面试中的复杂度分析》:)
              }
    

快速排序

  • 基本思想: (分治
    将选定的元素放到合适的位置,然后 递归 排序 被该元素分开的 前 后 两个子序列。(分出来的两个子序列可能不等长,相差很大,会影响性能)
  • 实现代码
        // 对arr[l...r]部分进行partition操作
        // 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
        template <typename T>
        int _partition(T arr[], int l, int r){

            // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
            swap( arr[l] , arr[rand()%(r-l+1)+l] );

            T v = arr[l];
            int j = l;
            for( int i = l + 1 ; i <= r ; i ++ )
                if( arr[i] < v ){
                    j ++;
                    swap( arr[j] , arr[i] );
                }

            swap( arr[l] , arr[j]);

            return j;
        }

        // 对arr[l...r]部分进行快速排序
        template <typename T>
        void _quickSort(T arr[], int l, int r){

            // 对于小规模数组, 使用插入排序进行优化
            if( r - l <= 15 ){
                insertionSort(arr,l,r);
                return;
            }

            int p = _partition(arr, l, r);
            _quickSort(arr, l, p-1 );
            _quickSort(arr, p+1, r);
        }
  • 优化:
    1. 标定点 随机选(保证两个子序列长度相近)(针对基本有序的序列,如果固定选最前面的元素,则分治的两个子问题不平衡,退化为O(n^2)的复杂度)
    2. 小规模排序,使用插入排序
  • 双路快排
    • 基本思想:针对键值重复过多时,分治的两个子序列不等长( < 与 > = 或 者 > 与 < = <与>= 或者 >与<= <>=><=),导致分治不平衡
    • 核心代码参考
      // 双路快速排序的partition
      // 返回p, 使得arr[l...p-1] <= arr[p] ; arr[p+1...r] >= arr[p]
      // 双路快排处理的元素正好等于arr[p]的时候要注意,详见下面的注释:)
      template <typename T>
      int _partition2(T arr[], int l, int r){
      
          // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
          swap( arr[l] , arr[rand()%(r-l+1)+l] );
          T v = arr[l];
      
          // arr[l+1...i) <= v; arr(j...r] >= v
          int i = l+1, j = r;
          while( true ){
              // 注意这里的边界, arr[i] < v, 不能是arr[i] <= v
              // 思考一下为什么?
              while( i <= r && arr[i] < v )
                  i ++;
      
              // 注意这里的边界, arr[j] > v, 不能是arr[j] >= v
              // 思考一下为什么?
              while( j >= l+1 && arr[j] > v )
                  j --;
      
              // 对于上面的两个边界的设定, 有的同学在课程的问答区有很好的回答:)
              // 大家可以参考: http://coding.imooc.com/learn/questiondetail/4920.html
      
              if( i > j )
                  break;
      
              swap( arr[i] , arr[j] );
              i ++;
              j --;
          }
      
          swap( arr[l] , arr[j]);
      
          return j;
      }
      
      // 对arr[l...r]部分进行快速排序
      template <typename T>
      void _quickSort(T arr[], int l, int r){
      
          // 对于小规模数组, 使用插入排序进行优化
          if( r - l <= 15 ){
              insertionSort(arr,l,r);
              return;
          }
      
          // 调用双路快速排序的partition
          int p = _partition2(arr, l, r);
          _quickSort(arr, l, p-1 );
          _quickSort(arr, p+1, r);
      }
      
  • 三路快排
    • 基本思想: 针对2路快排的加强,进一步解决键值重复过多的问题(增加一个等值区域)
    • 核心代码参考
      // 递归的三路快速排序算法
      template <typename T>
      void __quickSort3Ways(T arr[], int l, int r){
      
          // 对于小规模数组, 使用插入排序进行优化
          if( r - l <= 15 ){
              insertionSort(arr,l,r);
              return;
          }
      
          // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
          swap( arr[l], arr[rand()%(r-l+1)+l ] );
      
          T v = arr[l];
      
          int lt = l;     // arr[l+1...lt] < v
          int gt = r + 1; // arr[gt...r] > v
          int i = l+1;    // arr[lt+1...i) == v
          while( i < gt ){
              if( arr[i] < v ){
                  swap( arr[i], arr[lt+1]);
                  i ++;
                  lt ++;
              }
              else if( arr[i] > v ){
                  swap( arr[i], arr[gt-1]);
                  gt --;
              }
              else{ // arr[i] == v
                  i ++;
              }
          }
      
          swap( arr[l] , arr[lt] );
      
          __quickSort3Ways(arr, l, lt-1);
          __quickSort3Ways(arr, gt, r);
      }
      
      template <typename T>
      void quickSort3Ways(T arr[], int n){
      
          srand(time(NULL));
          __quickSort3Ways( arr, 0, n-1);
      }
      // 比较Merge Sort和双路快速排序和三路快排三种排序算法的性能效率
      // 对于包含有大量重复数据的数组, 三路快排有巨大的优势
      // 对于一般性的随机数组和近乎有序的数组, 三路快排的效率虽然不是最优的, 但是是在非常可以接受的范围里
      // 因此, 在一些语言中, 三路快排是默认的语言库函数中使用的排序算法。比如Java:)
      

归并排序和快速排序衍生的问题

* 求逆序对(归并排序)
* 求数组中的第N大元素

第四章 堆和堆排序

基本概念认识

  • 队列
    • 普通队列:先进先出
    • 优先队列:根据优先级出队
  • 优先队列应用 对动态的数据排序
  • 优先队列的三种实现方式
    实现方式入队出队
    普通数组O(1)O(n)
    顺序数组(元素有序)O(n)O(1)
    O(log n)O(log n)

堆的基本存储

  • 用数组存储二叉堆
  • 数组的索引次序 对应 二叉堆中 层序遍历次序
  • 对于完全二叉树,对第i个元素,其与其父,其子的关系
    • 根节点索引从0开始
      • parent(i) = (i-1)/2
      • left child (i) = 2*i +1
      • right child (i) = 2*i + 2
    • 根节点索引从1开始
      • parent(i) = i/2
      • left child (i) = 2*i
      • right child (i) = 2*i + 1

heapfy过程(堆的建立)

  • **基本思想:**从最后一个非叶子节点(索引为count/2,在根节点索引从1开始的情况下)开始,shiftdown(),自底向上ShiftDown操作,实现堆化。
  • 将n个元素逐个插入空堆中,算法复杂度为O(n log n),而heapfy过程算法复杂度O(n)
  • 实现代码
          // 构造函数, 通过一个给定数组创建一个最大堆
          // 该构造堆的过程, 时间复杂度为O(n)
          MaxHeap(Item arr[], int n){
              data = new Item[n+1];
              capacity = n;
   
              for( int i = 0 ; i < n ; i ++ )
                  data[i+1] = arr[i];
              count = n;
   
              for( int i = count/2 ; i >= 1 ; i -- )
                  shiftDown(i);
          }

ShiftUp(节点上移比较)

  • 代码
        void shiftUp(int k){
        while( k > 1 && data[k/2] < data[k] ){
            swap( data[k/2], data[k] );
            k /= 2;
        }
        }
    

ShiftDown(节点下移比较)

  • 代码
        void shiftDown(int k){
        while( 2*k <= count ){
            int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
            if( j+1 <= count && data[j+1] > data[j] )
                j ++;
            // data[j] 是 data[2*k]和data[2*k+1]中的最大值
    
            if( data[k] >= data[j] ) break;
            swap( data[k] , data[j] );
            k = j;
        }
        }
    

堆排序及其优化

  • 堆排序算法:
    • 创建堆: 一种通过不断insert()创建O(nlogn),一种传入数组用heapfy创建O(n)
    • 堆顶出堆: 将堆顶与堆尾互换,堆的size-1,并将新的堆顶(原堆尾)shiftDown()
    • 重复第二步
  • 堆排序的实现
    1. 借助insert()创建堆,不断将

      // heapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
      // 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
      // 整个堆排序的整体时间复杂度为O(nlogn)
      template<typename T>
      void heapSort1(T arr[], int n){
      
          MaxHeap<T> maxheap = MaxHeap<T>(n);
          for( int i = 0 ; i < n ; i ++ )
              maxheap.insert(arr[i]);
      
          for( int i = n-1 ; i >= 0 ; i-- )
              arr[i] = maxheap.extractMax();
      
      }
      
      
    2. 借助heapfy() 创建堆

       // heapSort2, 借助我们的heapify过程创建堆
        // 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 实践复杂度为O(nlogn)
        // 堆排序的总体时间复杂度依然是O(nlogn), 但是比上述heapSort1性能更优, 因为创建堆的性能更优
        template<typename T>
        void heapSort2(T arr[], int n){

            MaxHeap<T> maxheap = MaxHeap<T>(arr,n);
            for( int i = n-1 ; i >= 0 ; i-- )
                arr[i] = maxheap.extractMax();

        }
  • 堆的实现细节
    ShiftUp和ShiftDown种使用复制操作替换swap操作

索引堆

  • 基本思想: 分索引数组和数据数组,数据数组存数据元素。索引数组按堆的次序(比较的是实际元素的键值)存对应节点的索引(地址)。ShiftUP和ShiftDown中比较(实际元素的值),交换对应的索引(存储地址),而不交换实际的元素。
  • 一点理解: 维护了一个数组,既能有堆的特性(取最值),又能保持原有数组的存储次序(数据数组的索引是原数组的+1)。
  • ShfitUp()
        // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
        void shiftUp( int k ){
        while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
            swap( indexes[k/2] , indexes[k] );
            k /= 2;
        }
    }
    
  • ShiftDown()
        // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
        void shiftDown( int k ){
            while( 2*k <= count ){
                int j = 2*k;
                if( j + 1 <= count && data[indexes[j+1]] > data[indexes[j]] )
                    j += 1;
                if( data[indexes[k]] >= data[indexes[j]] )
                    break;
                swap( indexes[k] , indexes[j] );
                k = j;
            }
        }
    
  • 增删实现 (插入操作 不是很理解)
        // 向最大索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
    // 传入的i对用户而言,是从0索引的
    void insert(int i, Item item){
        assert( count + 1 <= capacity );
        assert( i + 1 >= 1 && i + 1 <= capacity );
        i += 1;
        data[i] = item;
        indexes[count+1] = i;
        count++;
        shiftUp(count);
    }
    
    // 从最大索引堆中取出堆顶元素, 即索引堆中所存储的最大数据
    Item extractMax(){
        assert( count > 0 );
    
        Item ret = data[indexes[1]];
        swap( indexes[1] , indexes[count] );
        count--;
        shiftDown(1);
        return ret;
    }
    
  • 应用: 最小生成树Prim算法中,用来维护 每个节点对应的最小横切面的权重【同时能输出当前所有节点最小横切面中的最小横切面】

涉及堆 的相关问题

  • 多路归并排序 多个元素同时比较的时候用,最小(大)堆
  • d叉堆 d-ary heap
  • 最大最小队列 (最大堆和最小堆同时维护??)
  • 二项堆
  • 斐波那契堆

桶排序

基本思想: 分配 + 收集 (先排序低位再排序高位)

排序总结

排序平均时间复杂度原地排序额外空间稳定排序
插入排序(Insertion Sort)O(n^2)O(1)
归并排序(Merge Sort)O(nlogn)O(n)
快速排序(Quick Sort)O(nlogn)O(logn)
堆排序(Heap Sort)O(nlogn)O(1)
  • 不同排序算法的选择
    • n较小:直接插入排序或直接选择排序
    • 基本有序序列,直接插入,冒泡,随机的快速排序
    • n较大,应选复杂度好的:快速排序,归并排序, 堆排序。
      • 快速排序性能平均最好,堆排序更少的辅助空间,归并排序是稳定的排序。(当排序数少于一定时如16,调用直接插入排序

其他参考
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

C语言中的14种排序
14中排序动画演示


第五章 二分搜索树

5-1 二分查找法

  • 基本思想: (递归分治)对有序序列,不需要逐一比较,只要比较中间值,然后确定目标值的可能区域,再在可能区域递归二分查找。
  • 细节:
    1. 计算中间值时, 注意防止越界(基本类型的范围)。
      • 合理写法: int mid = left + (right-left)/2
      • 危险写法: int mid = (left + right)/2
    2. 二分查找返回的只有一个索引,而序列中可能存在重复值。如何返回所有重复值?
      • 练习:对于存在重复值的序列,返回目标值索引的floor(第一个索引)和ceil(最后一个索引)
  • 改进:(选不同的分割点)
    • 插值查找 (按比值分割)
      • mid = left + (right-left)*{(key-a[left])/(a[right]-a[left])}
    • 斐波那契查找
      • mid = left + F_block - 1 (黄金分割)

5-2 二分搜索树

  • 二分搜索树的定义:

    • 左子树>(<) 根 >(<) 右子树 [一般不考虑键值重复的问题]
  • 二分搜索树的优势

    • 高效((O(nlogn))的维护数据的有序性:min,max,floor,ceil,rank,select
  • 二分搜索树的节点插入

    • 实现方式:
      1. 递归(返回子树根节点)[从根节点开始搜索,找到key值则替换,未找到则根据比较关系递归子树]
          // 向以node为根的二分搜索树中, 插入节点(key, value), 使用递归算法
          // 返回插入新节点后的二分搜索树的根
          private Node insert(Node node, Key key, Value value)\{
      
              if( node == null ){
                  count ++;
                  return new Node(key, value);
              }
      
              if( key.compareTo(node.key) == 0 )
                  node.value = value;
              else if( key.compareTo(node.key) < 0 )
                  node.left = insert( node.left , key, value);
              else    // key > node->key
                  node.right = insert( node.right, key, value);
      
              return node;
          }
      
      1. 迭代
        • 当作练习
  • 二分搜索书的查找

    • 基本思想: 二分查找
  • 二分搜索树的遍历

    • (深度优先遍历)
      • 前序遍历,中序遍历,后序遍历
    • (广度优先遍历)
      • 层序遍历
  • 删除最大值,最小值

    • 实现:
      • 递归:(不断递归(左/右)子树,直到节点没有(左/右)子树,删除该节点,返回其的(右/左)子树赋值给上一节点的(左/右)子树)
  • 删除Key对应的节点

    • 实现:
      • 递归
        • 从根节点开始递归搜索
          • 如果目标值在根节点则删除根节点,选新的中间值上位(左子树最右(大/小)的节点,右子树最左(小/大)的节点)
          • 否则根据比较次序,选择(左/右)子树递归搜索,根节点的(左/右)子树 = 返回删除最值后的子树的根节点

5-3 二分搜索树的特性

  • 顺序性

    • minimum/maximum
    • successor/predecessor 后继/前继
    • floor/ceil (存在键值重复时,索引的范围)
    • rank/select 已知key获取排名/已知排名获取key,value
  • 局限性:(不平衡) 依照顺序或逆序插入元素,二分搜索树退化为链表(跟节点为第一个元素,在最左,或最右)。

  • 支持重复键值的二分搜索树

    • 思路一: 让键值重复节点的右子树或左子树包含键值重复的节点。
    • 思路二: 每个节点增加一个区域(链表或数组),存键值相等的元素

二叉平衡树

- 平衡二叉树:
    - 左右子树高度差不超过1的二叉搜索树,即平衡二叉树所有结点的平衡因子绝对值不超过1(平衡因子 = 结点左子树的高度 - 结点右子树的高度)。
- 实现方式:
    - AVL树
    - 红黑树
    - 2-3树
    - Splay树

AVL 树

  • 基本思想,每插入一个节点,自底向上维护树的平衡,以及每个节点的平衡因子

  • 平衡调整的四种类型:

    • LL型 右旋调整 调整后涉及到的节点的平衡因子(bf)均为0
    • RR型 左旋调整 调整后涉及到的节点的平衡因子(bf)均为0
    • LR型 先左旋调整至LL型, 然后右旋调整 bf的维护如下:
    • RL型 先右旋调整至RR型, 然后左旋调整
      在这里插入图片描述
  • 插入操作思路整理:

    • 考虑当前节点的子树是否添加了新节点
      • 没有 则不用维护,直接返回
      • 左子树添加了新节点 >>> 当前节点的平衡因子要加一( b f = b f + 1 bf= bf+1 bf=bf+1)
        • b f > 1 bf>1 bf>1(需要平衡调整) >>> 进行左平衡调整
      • 右子树添加了新节点 >>> 当前节点的平衡因子要减一( b f = b f − 1 bf = bf -1 bf=bf1)
        • b f < − 1 bf<-1 bf<1,需要平衡调整 >>> 进行右平衡调整
  • 左平衡调整(cur.bf > 1)

    • 看cur的左子节点L 的平衡因子 L.bf
      • L.bf == 1 >>> LL型 >>> 右旋操作
      • L.bf == -1 >>> LR型 >>> 先左旋调整至LL型,然后右旋调整
  • 右平衡调整(cur.bf < -1)

    • 看cur节点的右孩子节点R 的平衡因子R.bf
      • R.bf == -1 >>> RR型 >>> 左旋操作
      • R.bf == 1 >>> RL型 >>> 先右旋调整至RR型,然后左旋调整
  • 插入操作代码

	//插入操作
	void insert(K key,V value) {
		taller = false; //增加了新的一层??
		root = __insert(root,key,value);
	}
	
	//返回插入新节点后的根节点
	private AVLNode __insert(AVLNode curNode,K key,V value) {
		if(curNode==null) {
			curNode = new AVLNode(key,value);
			taller = true; //增加了新节点
			return curNode;//
		}
		else {
			if(key.compareTo(curNode.key)==0) {
				return curNode;//节点已经存在,返回(不覆盖旧值)
			}
			else if(key.compareTo(curNode.key)<0) {
				curNode.left = __insert(curNode.left,key,value); //往左子树新增节点
				if(taller) {//增加了新节点
					switch(curNode.bf) {
						case 0: //新增节点没有打破平衡,但bf+1
							curNode.bf = 1;//左边新加一个点
							taller = true;//此处很重要,自底 向上传递平衡信息
							break;
						case 1:  //本来左子树多一个结点,然后taller为true,左子树又增加了一个结点
							curNode = leftBalance(curNode); //左平衡时必须保证 curNode具有左孩子
							taller = false; //左平衡后 达到平衡
							break;
						case -1: //新增节点使得curNode左右平衡
							curNode.bf = 0;
							taller = false;
							break;
					}
				}
				return curNode;
			}
			else { //往当前节点的右子树增添节点
				curNode.right = __insert(curNode.right,key,value);//往右子树增加节点
				if(taller) {
					switch(curNode.bf) {
						case 0:
							curNode.bf = -1;
							taller = true;
							break;
						case 1:
							 curNode.bf = 0;
							 taller = false;
							 break;
						case -1:
							curNode = rightBalance(curNode);
							taller = false;
							break;		
					}
				}
				return curNode;
			}
		}
	}
  • 左平衡调整代码
	/*
	 * 左平衡操作
	 */
	//[当前curNode的bf>0,又再左子树增加节点时需要左平衡操作]
	AVLNode leftBalance(AVLNode curNode) {
		AVLNode L = curNode.left;
		if(L.bf==1) { //LL型需要左旋
			curNode.bf = 0;//右旋平衡
			L.bf = 0;//右旋平衡
			return rightRotate(curNode);
		}
		else if(L.bf==-1) {//LR型需要 先左旋  后右旋
			AVLNode LR = L.right; //左孩子的右孩子
			//以下代码不是很理解,,,画一下实际例子可以明白,,但为啥子LR.bf不会一直被维护为0?(因为LR不一定是新增节点?)
			switch(LR.bf) {
				case 1:
					curNode.bf = -1;
					L.bf = 0;
					break;
				case 0:
					curNode.bf = 0;
					L.bf = 0;
					break;
				case -1:
					curNode.bf = 0;
					L.bf = 1;
					break;
			}
			LR.bf = 0;
			curNode.left = leftRotate(curNode.left);
			return rightRotate(curNode);
		}
		else {//不需要左平衡处理
			return curNode;
		}
	}
  • 右平衡调整代码
	//右平衡操作
	/*
	 * curNode bf本已经-1的基础上,在右方又新增节点
	 */
	AVLNode rightBalance(AVLNode curNode) {
		AVLNode R = curNode.right;
		if(R.bf==-1) { //RR型 
			curNode.bf = 0;
			R.bf = 0;
			return leftRotate(curNode);
		}
		else if(R.bf==1) {//RL型
			AVLNode RL = R.left;
			switch(RL.bf) {
				case 0:
					curNode.bf = 0;
					R.bf = 0;
					break;
				case 1:
					R.bf =-1;
					curNode.bf = 0;
					break;
				case -1:
					curNode.bf = 1;
					R.bf = 0;
					break;
			}
			RL.bf = 0;
			curNode.right = rightRotate(R);
			return leftRotate(curNode);
			
		}
		else {
			return curNode;
		}
	}
  • 左旋操作
	//左旋操作
	AVLNode leftRotate(AVLNode curNode) {
		AVLNode subRoot = curNode.right;
		curNode.right = subRoot.left;
		subRoot.left = curNode;
		return subRoot;
	}
  • 右旋操作
	//右旋操作
	AVLNode rightRotate(AVLNode curNode) {
		AVLNode subRoot = curNode.left;//左孩子上位
		curNode.left = subRoot.right;//把你的右孩子给我,作为我的左孩子(保证排序性)
		subRoot.right = curNode;//你上位后我成为你的有孩子
		return subRoot;
	}
  • 其他操作实现:
    • 可参考<<数据结构从应用到时间(Java版)>>

5-5 多路查找树

  • 多路查找树的意义

    对于树来说,一个结点只能存储一个元素,那么在元素非常多的时候,就会使得要么树的度非常大(结点拥有子树的个数的最大值),要么树的高度非常大,甚至两者都必须足够大才行,这就使得内存存取外存次数非常多,这显然成了时间效率上的瓶颈,这迫使我们要打破每一个结点只存储一个元素的限制,为此引入了多路查找树的概念

  • 2-3 树

    • 每个结点都具有两个孩子(我们称它为 2 结点)或三个孩子(我们称它为 3 结点)的树
    • 特性
      一个 2 结点 包含一个元素和两个孩子(或没有孩子),且左子树数据元素小于该元素右子树数据元素大于该元素
      一个 3 结点 包含一小一大两个元素和三个孩子(或没有孩子)
      如果有 3 个孩子的话
      左子树包含小于较小元素的元素
      右子树包含大于较大元素的元素
      中间子树包含介于两元素之间的元素
  • 2-3-4 树

  • B 树

5-6 树形问题和更多树。

  • 平衡二叉树和堆的结合Treap
  • trie :查找效率与单词长度有关,与单词中的单词数量无关
  • 其他树形(递归)问题:
    • 一条龙游戏
    • 8 数码问题
    • 8 皇后问题
    • 数独
    • 搬运工
  • 更多树的结构
    • KD树
    • 区间树
    • 哈夫曼树

第六章:并查集

6-1 并查集基础

  • 解决的问题:
    • 连接问题, 数学中集合类的实现,
    • 路径问题
  • 并查集的基本方法
    • union(p,q) 将p与q 联合 (同属一个集合(连通分支))
    • find§ 查找p所在集合序号
    • isConnect(p,q)
  • 并查集的实现
    • 数组实现(QuickFind) (数组索引为元素键值,数组值为对应的集合号)
      • union(p,q) 需要遍历更新所有元素的集合号,时间复杂度O(n)
      • find§ 直接数组下标查找,O(1)
      • isConnect() 复杂度O(1)
    • 树形实现(数组索引为元素键值,数组值为该节点的父节点的索引)
      • union(p,q) 时间复杂度O(h),h为树的高度
      • find§ 需要回溯到(所在分支的)跟节点,O(h),h为树的高度
      • isConnect() 复杂度O(h,h为树的高度
      • 树形实现中 union() 的三种实现方式(不断改进(降低)树的高度h)
        • 随机合并
        • 基于size的优化
          • 维护一个size数组,存每个分支的元素多少
          • 将元素较少的树合并到元素较多的的树 新的分支size = size1 + size2
        • 基于rank的优化
          • 维护一个rank数组,存每个分支对应树的高度
          • 将高度小的和平到高度大的
      • find() 过程中的 路径压缩(降低树的高度)
        • 基本思想: 基于树形实现的并查集,在find()时,需要回溯到分支树的根节点,在回溯的过程,可以将树的高度进行调整。
        • 具体实现思路: 回溯过程中,将当前节点的父节点不断向上更新
        • 代码
        // 查找过程, 查找元素p所对应的集合编号
        // O(h)复杂度, h为树的高度
        private int find(int p){
            assert( p >= 0 && p < count );
    
            // path compression 1
            while( p != parent[p] ){
                parent[p] = parent[parent[p]]; //指向父节点的父节点
                p = parent[p];
            }
            return p;
    
            // path compression 2, 递归算法
    //            if( p != parent[p] )
    //                parent[p] = find( parent[p] );
    //            return parent[p];
        }
    
  • 并查集的应用

第七章: 图

7-1 图论基础

  • 图的应用
    • 通信网路
    • 社交网络
    • 状态机等
  • 图的分类
    • 无权图与有权图
    • 有向图与无向图
  • 图的表示
    • (n,m) n个顶点,m条边
    • 邻接矩阵 n ∗ n n * n nn矩阵 适合稠密图
    • 邻接表 适合稀疏图
    • 其他

7-2 相邻点迭代器

  • 图遍历过程中,避不开的便是遍历当前节点的相邻节点
  • 迭代器的思想: 对外提供访问数据的功能,同时避免内部数据直接暴露在外
  • 复杂度分析
    • 邻接矩阵 O(n)
    • 邻接表 O(m)

7-4 图的算法框架

  • 读图方式(仅限本课程)
    • 不断添加边(需要知道顶点数)

7-5 深度优先遍历和联通分量

  • 深度优先遍历(DFS)
    • 基本思想 递归
    • 复杂分析
      • 邻接表O(V+E)
      • 邻接矩阵O(V^2)
  • 求连通分支
    • 维护一个size为顶点数的vis数组 (可以存每个节点是否被访问,也可以每个节点存属于哪个分支)
    • 对每一个没有被访问的节点进行深度优先遍历
  • 寻路
    • 维护一个from 数组 ,from[i]存第i个节点在dfs(s)过程中的上一个节点
    • s s s 节点开始 dfs()
    • 从目的点d 开始,根据from数组回溯,得到s到d的路径

7-7 广度优先遍历和最短路径

  • 广度优先遍历
    • 基本思想 迭代
    • 实现细节
      • 处理环: 相对于树来说 图存在环的可能,所以遍历的时候是将加入队列的元素标记为已访问,而不是访问结点时才标记。
    • 复杂度分析
      • 邻接表O(V+E)
      • 邻接矩阵O(V^2)
    • 求 无权图的 最短路径
      • 维护一个from(回溯)数组,from[i]存第i个节点到dfs(s)过程中的上一个节点
      • 维护一个order数组, order[i] 存 第i个节点在从s开始的bfs过程中的第几层(最短距离)
      • s s s 节点开始 bfs()
      • 从 d 开始,根据from数组回溯

7-8 更多无权图的应用

  • 迷宫生成,ps抠图等

第八章:最小生成树

8-1 最小生成树问题和切分定理

  • 最小生成树的应用
    • 电缆布线设计,网络设计,电路设计等。
  • 最小生成树的适用范围
    • 主要针对 带权 无向 连通图
  • 切分定理
    • 相关概念
      • 切分
      • 横切边
    • 切分定理: 给定任意切分,横切边中权值最小的边必然属于最小生成树

8-2 Prim算法及其优化

  • Lazy Prim 算法
    • 基本思想: 迭代/递归 贪心(每次根据切分定理得到局部最优) 动态规划?
    • 算法流程
      • 递归/迭代遍历节点
      • 将节点集合 分为 已经被访问过的节点未被访问过的节点,并将切分这两个集合的横切边存入最小堆中
      • 每次选择最小堆中的横切边 出堆
        • 若 该横切边的两个端点均 已经被访问 , 丢弃该边, 不做操作
        • 否则, 将该边加入最小生成树中,继续遍历 该边中未被访问的点
    • lazy体现在(局限性):
      • 最小堆中,维护的横切边中,有很多不再时横切边的 没有及时丢弃。
      • 复杂度 O(ElogE)
  • Prim算法的优化
    • 索引堆 (pq)来维护
      • 每个已经被访问的点对应的最小横切边 的权重。pq.get(i) 表示第i个节点关联的当前最小横切边权重
      • 上述横切边集合中 的 最小横切边的权重 (最小中的最小) (及堆顶)
      • 用 edgeTo 辅助维护 边的信息(起始点)
    • 用 vis 数组来维护 节点是否已被访问
    • Prim 算法流程
      • 初始化: 将起始点的横切边 的权值加入索引堆pq中,起始点标记为 已经被访问
      • 循环:
        • pq中 最小权值的横切边e 加入到最小生成树的 边集合
        • 递归 选出来边e的两个端点中 未被访问的节点v:
          • 端点v 标记为 已经被访问
          • 遍历端点v 的 邻接边adjEdge,邻接边的另一个端点w
            • 保证**pq[w]**小((与w节点关联的当前最小横切边的权重)
              • pq[w]) 为空,则将 adjEdge 的权重 更新至 pq[w]
              • 或者若adjEdge的权重小于 pq[w] 则将 adjEdge 的权重更新至pq[w]
    • 复杂度分析 O(ElogV)

8-3 Krusk算法

  • 基本思想: 贪心 不断将不与已添加的边构成圈的最小边 加入到生成树边集合种
  • 数据结构
    • 用最小堆,维护边集合,保证每次能取出剩余边中的最小边
    • 用并查集,维护节点在当前生成树边集合下的连通度,判断新增边是否构成环
  • 算法流程
    • 对边进行排序(复杂度O(ElogE)),用最最小堆来维护边集合。
    • 将最小边出堆,并加入生成树的集合。
    • 循环开始:
      • 将 剩余边集合中 最小边 出堆
        • 判断改边与 生成树边集合中的边是否 构成环 (用并查集判断)
          • 若成环 抛弃, 继续找下一个边
          • 将改边加入生成树的边集合
  • 复杂度分析 O(ElogE)

8-6 最小生成树算法的思考

  • 其他最小生成树的算法
    • Vyssotsky’s Algorithm 将边逐渐添加到生成树中,一旦形成环,删除环中权值最大的边

第九章:最短路径

9-1 最短路径问题和松弛操作

  • 最短路径的应用
    • 路径规划
    • 工作任务计划
  • 广度优先遍历 求 无权图 的最短路径
    • 最短路径树 (单源最短路径:一点到所有其他点的最短路径)
  • 松弛操作(Relaxation)
    • 考虑经过该节点的 是否有到达其他节点的更短的路径

9-2 Dijkstra算法的思想

  • 适用条件
    • 图中不能有负权边
    • 复杂度O(E log V)
  • 实现
    • 数据结构:
      • 索引堆: 维护每个节点当前被到达的最短路径, 以及 所有最短路径中的最短路径
        • 辅助:访问标记数组: 存当前节点是否进行了松弛操作
      • 回溯数组: 存到达该节点的最短路径中的上一个节点
  • 算法流程
    • 初始化数据结构:
      • 索引堆IndexMinHeap(imh)imh[w] 表示第w个节点当前最短路径的权值
      • 访问数组 boolean[] marked, marked[i] 表示第 i 个节点是否进行过松弛操作,初始都为false
      • 回溯数组int[] from, from[i] 表示目的点为第i个节点的上一个节点,初始均赋值为-1
    • 初始化:将 源节点 s s s 到自身的路径的权重 加入索引堆
    • 循环(直至索引堆为空)
      • 当前距离源节点 s s s 最近的节点 v v v , 出堆
        • 如果 v v v 没有进行过松弛操作(marked[v]==false), 则对其进行 松弛操作
          • v v v 所有邻节点 w w w :
            • 如果 w w w 没有进行过松弛操作( m a r k e d [ v ] = = f a l s e marked[v]==false marked[v]==false
              • s s s 经过 v v v 到达 w w w 的最短路径为: d i s = d i s ( s , v ) + d i s ( v , w ) dis = dis(s,v) + dis(v,w) dis=dis(s,v)+dis(v,w)
              • if from[w]==-1||ids<imh[w]
              • imh[w] = dis
              • from[w] = v
          • marked[v] = true
  • 实现代码:
    • 初始化及核心代码
        //初始化  
        marked = new boolean[graph.V()];
	    from = new int[graph.V()];
		for(int i=0;i<graph.V();i++) {
			from[i] = -1;
			marked[i] = false;
		}
        //Dijkstra
		IndexMinHeap<Weight> imh = new IndexMinHeap<>(graph.V());
		imh.insert(s, (Weight)(Number)(0.0));
		while(!imh.isEmpty()) {
			int v = imh.extractMinIndex();
			if(!marked[v]) 
				Relaxation(v,imh); //Relaxation松弛操作
		}
        
  • 松弛操作
        //松弛操作
        void Relaxation(int v, IndexMinHeap imh) {
		marked[v] = true;
        //遍历邻边
		for(Edge<Weight> e : (List<Edge>)graph.adj(v)) {
			int w = e.getOther(v);
			if(!marked[w])
			{
				Double dis = e.getW().doubleValue()+(((Number) imh.getItem(v)).doubleValue());
				if(from[w] == -1||imh.getItem(w).compareTo(dis)>0) {
					if(!imh.contain(w))
						imh.insert(w,dis);
					else 
						imh.change(w, dis);
					from[w] = v;
				}
				
			}
		}

9-3 负权边和Bellman-Ford算法

  • 负权边带来的问题

    • 每次选择的并不是
    • 负权环
  • Bellman-Ford 单源最短路径算法

    • 前提: 图中不能有负权环
    • Bellman-Ford 可以判断图中是否有负权环
      • 如果一个图没有负权环,则:
        • 从一个顶点到另一个顶点的最短路径,最多经过所有( V V V)的顶点,即有 V − 1 V-1 V1条边
      • 否则, 存在 顶点 经过了两次, 即 存在权环
    • 复杂度 O ( E V ) O(EV) O(EV)
  • Bellman-Ford 基本思想

    • 对所有点进行 v − 1 v-1 v1 次 松弛操作,如果还可以继续松弛,则存在负权环
    • 对所有点(V)进行松弛操作, 遍历所有的边(E),复杂度为O(VE)??
  • 算法流程

    • Initial: from[s] = s; distTo[s] = 0;//初始化源节点 信息
    • for pass = 1:G.V()-1 : //遍历 G.V()-1 遍
      • for(int v=0; v < G.V();v++) : //每一遍对每个节点进行一次松弛操作
        • Relaxation(v):
          • 如果 v v v可达,遍历 v v v 的所有邻接点 w i wi wi
            • d i s = d i s ( s , v ) + d i s ( v , w i ) dis = dis(s,v) + dis(v,wi) dis=dis(s,v)+dis(v,wi)
            • if from[w]==-1||dis < dis(s,w) :
              • dis(s,w) = dis;
              • from[w] = v;
    • //对每个节点再进行一次松弛操作,如果路径还能再减,则存在负权环
  • 实现代码

        // Bellman-Ford的过程
        // 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
        for( int pass = 1 ; pass < G.V() ; pass ++ ){

            // 每次循环中对所有的边进行一遍松弛操作
            // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
            for( int i = 0 ; i < G.V() ; i ++ ){
                // 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边
                for( Object item : G.adj(i) ){
                    Edge<Weight> e = (Edge<Weight>)item;
                    // 对于每一个边首先判断e->v()可达
                    // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
                    // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
                    if( from[e.v()] != null && (from[e.w()] == null || distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue()) ){
                        distTo[e.w()] = distTo[e.v()].doubleValue() + e.wt().doubleValue();
                        from[e.w()] = e;
                    }
                }
            }
        }
  • 检测是否有负权环
   // 判断图中是否有负权环
    boolean detectNegativeCycle(){

        for( int i = 0 ; i < G.V() ; i ++ ){
            for( Object item : G.adj(i) ){
                Edge<Weight> e = (Edge<Weight>)item;
                if( from[e.v()] != null && distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue() )
                    return true;
            }
        }

        return false;
    }

  • queue-based Bellman-Ford

    • 对Bellman-Ford的优化
  • 其他

    • 处理负权边通常是针对 有向图, 因为无向图的负权边 本身就是一个负权环

9-4 更多和最短路径相关的思考

  • 单源最短路径算法总结
算法边的权重图的有向性复杂度
dijkstra无负权边有向,无向图均可 O ( E l o g V ) O(ElogV) O(ElogV)
Bellman-Ford无负权环有向图,或无负权边的无向图 O ( E l o g V ) O(ElogV) O(ElogV)
利用拓扑排序有向无环图(DAG)有向图 O ( V + E ) O(V+E) O(V+E)
  • 所有对最短路径算法
    • Floyed算法, 处理无负权环的图,复杂度 O ( V 3 ) O(V^3) O(V3)
  • 最长路径算法
    • 不能有正权环
    • 无权图的最长路径问题是指数难度的
    • 对于有权图,不能使用Dijkstra求最长路径问题
    • 可以使用Bellman-Ford (把所有路径都取负)

第十章:结束语

线性问题 >> 树形问题 >> 图形问题

  • 线性问题

    • 排序问题
      • O ( n 2 ) O(n^2) O(n2) 选择排序 插入排序
      • O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)) 归并排序 快速排序
  • 更多的算法问题

    • 数据结构相关
      • 双向队列
      • 斐波那契堆
      • 红黑树
      • 区间树
      • KD树
    • 具体领域相关
      • 数学: 数论 计算几何
      • 图论: 网络流
  • 算法设计相关

    • 分治算法
      • 归并排序,快速算法, 树结构
    • 贪心
      • 选择排序; 堆 ; Kruskal ; Prim; Dijkstra ;…
    • 递归回溯
      • 树的遍历
      • 图的遍历
    • 动态规划
      • 思想: 最优子结构
      • Prim ; Dijkstra
  • 大家加油!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值