Basic Algorithms - 1

Posted by emengjzs Blog on January 3, 2017

算法,必须把基础的部分重复再重复,不管喜不喜欢。

等价类

两种算法:QuickFind, QuickUnion

  • 基于数组,权衡合并和查找操作时间复杂度,两者不能兼得。

QuickFind

  • 所有等价类均指向同一个元素,合并时需要遍历修改标记。

QuickUnion

  • 每个类对应一个父节点,形成树。

  • Weighted quick-union ,为了将各个节点到根路径均匀,避免出现长路径,将子节点数作为衡量标准(权重),将权重小的树挂接到权重大的树上。需要另外记录各个节点权重,合并时需要更新权重

  • Weighted quick-union by height, 将以节点为根节点的树高度作为权重,仅当两个树相同高度时才需要更新权重。

  • Path compression,为了减小节点到根路径长度,将路径进行压缩。在搜索时将所有访问过的节点全部挂载到根节点,需要遍历两次。

  • 另一种压缩路径是PathHalving, 一步两步,将节点挂载到祖父节点,适量权衡更新和查找开销。

    int find(int p) {
      while (p != _parent[p]) {
        // notice the two statement order!
        _parent[p] = _parent[_parent[p]];
        p = _parent[p];
      }
      return p;
    }
    

Sorting

选择排序

  • 关键点:每次在剩余的元素中选取最大/小的元素,然后排在剩余元素的首位

// like <T, Compare<T>> in Java
template<typename T, template <typename> class Compare>
  void selection_sort(std::vector<T>& arr, const Compare<T>& cmp) {
  for (int i = 0; i != arr.size(); ++ i) {
    int max_index = i;
    for (int j = i; j != arr.size(); ++ j) {
      if (cmp(arr[i], arr[j]) < 0) {
        max_index = j;
      }
    }
    if (max_index != i) {
      std::swap(arr[max_index], arr[i]);
    }
  }
}

// usage
vector<int> arr{ 5, 4, 3, 2, 1 };
selection_sort(arr, algs::less<int>());

插入排序

  • 关键点: 每次将一个元素插入在之前已排序的列表中的合适位置上。

  • Move rather than exchange. 对元素进行移动,避免过多的两两交换,相比其他换位算法减少赋值次数。

    • 先拿出(复制)一个待插入元素,这样在数组中的该元素值可以被之后的操作覆盖。

      T temp = a[i];
      
    • 移动时,只考虑目标操作元素,源目标的值不用考虑清空。

      // The item a[i] is moved to a[i + 1], ignore what is remained in a[i]
      a[i + 1] = a[i];  
      
    • 最后,最后一次移动操作的源目标位置填上刚才复制出的元素

    • v1:

      template<typename T, template<typename> class Compare>
        void insertion_sort(std::vector<T>& arr, const Compare<T>& cmp) {
        for (int i = 0; i < arr.size(); ++ i) {
          T temp = arr[i];
          int j = i - 1;
          while (j >= 0 && cmp(temp, arr[j]) > 0) {
            arr[j + 1] = arr[j];
            -- j;
          }
          arr[j + 1] = temp;
        }
      }
      

  • sentinel. 设置一个充分条件(哨兵),在退出循环时必定已经完成遍历任务 。

  • sorted detected. 先扫描一遍,若已排序则结束任务,否则,拿取最大/小元素放置在首位,使用冒泡排序反向遍历

  • v2:

    void insertion_sort(std::vector<T>& arr, const Compare<T>& cmp) {
      // Check if it is sorted.
      bool sorted = true;
      for (int i = arr.size() - 1; i > 0; -- i) {
        if (cmp(arr[i], arr[i - 1]) > 0) {
          std::swap(arr[i], arr[i - 1]);
          sorted = false;
        }
      }
      if (sorted) return;
    
      // Original insertion sort, notice the i is begin from 1, not 0!
      for (int i = 1; i < arr.size(); ++ i) {
        T temp = arr[i];
        int j = i - 1;
        // Don't have to check if j is out of bound.
        while (cmp(temp, arr[j]) > 0) {
          arr[j + 1] = arr[j];
          -- j;
        }
        // Notice that the index we put temp in is j + 1, not j ! 
        // When a variable breaks from while/for, the value is already invalid
        arr[j + 1] = temp;
      }
    }
    
  • 使用二分查找,找出待插入位置,减少比较次数。

快速排序

  • Partitioning. 选取一个作为分界点,务必设置为引用

  • 使用两个指针,i, j在遍历时以low和high为界,不要以j, i为界,因为在循环结束时i,j可能已经不合法。

  • 在扫描时即使遇到和中枢元素相等的元素时也停止循环并进行交换,防止在数组有大量相等元素时退化为线性时间。

  • v1

    template <typename T, template <typename> class Compare>
    int _partition(std::vector<T>& arr, const Compare<T>& cmp, int low, int high) {
      T& pivot = arr[low];
      int i = low + 1, j = high;
      while (i < j) {
        // Not 'i < j' because j may be invalid after breaking from loop
        while (i < high && cmp(arr[i], pivot) < 0) ++ i;
        // Not 'j < i' because j may be invalid after breaking from loop
        while (j > low && cmp(arr[j], pivot) > 0) -- j;
        if (i < j) {
          std::swap(arr[i], arr[j]);
        }
      }
      std::swap(arr[low], arr[j]);
      return j;
    }
    
  • 三分中路. 选取一个元素,将小于的元素全部置于左边,大于元素全部置于右边,剩余相等的元素必定在中间。取两个边界less和greater,less之下的小于,greater之上的大于。小的对换时,换来的元素一定小于等于中枢元素,而大的兑换时,换来的元素未进行比较,故遍历的指针不能前进,相对的,遍历到greater边界为止即可。