算法与数据结构

算法与数据结构

标签(空格分隔): 数据结构 算法


复杂度为o(n)的一些排序算法,往往都比较简单,容易在第一时间内想到的排序算法

排序算法

选择排序

template<typename T>
void SelectSort(T arr[], int n){
for( i = 0; i < n; i++){
   int minIndex = i;
   for(j = i + 1; j < n; j++){
        if (arr[j] < arr[i])
            minIndex = j;
        swap(arr[i], arr[minIndex]);
        }
    }
    return;
}

插入排序

插入排序的特性是其对于近乎有序的排序性能甚至比o(nlogn)还要优,近乎于o(n)性能
原版:

template<typename T>
void insertionSort(T arr[], int n){
    //循环从i=1开始,因为第0个元素已经是有序的
    for( int i = 1 ; i < n ; i ++ ) {

        // 寻找元素arr[i]合适的插入位置
        for( int j = i ; j > 0 ; j-- ){
            if( arr[j] < arr[j-1] )
                swap( arr[j] , arr[j-1] );
            else
                break;
        }
    }
}

改进版:用赋值代替了交换的操作,减小了算法复杂度。

template<typename T>
void insertionSort(T arr[], int n){
    for( int i = 1 ; i < n ; i ++ ) {
        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;
}

希尔排序

希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
但是不同的增量序列会对算法性能产生不同的影响

复杂度为o(nlogn)的一些排序算法

归并排序(自顶向下的递归归并排序)

不断地1/2分组,直至每个组只有1个元素。再进行归并操作。
总共有log(N)个Level,每个Level用o(N)的排序算法,因此算法时间复杂度为o(NlogN)
需要三个索引i,j,k 分别指示最左边、中间、以及最右边的数据。
需要复制一份序列副本来辅助排序,因此在空间复杂度上有一定程度的增加
原版:

//归并排序
template<typename T>
void mergeSort(T arr[], int n){
    __mergeSort( arr , 0 , n-1 );
}

//递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){
    //每个分组只剩下1个元素
    if( l >= r )
        return;
    //二分分组
    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    __merge(arr, l, mid, r);
}

//将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename  T>
void __merge(T arr[], int l, int mid, int r){

    //复制一份序列的副本作为辅助
    T aux[r-l+1];
    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];
    //定义索引
    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){
    //先确认i,j索引的合法性,保证其分别还在[l,mid],[mid,r]的范围内。
    //若i已经超出了范围而k仍未遍历完成,说明j部分还有需要归并的数据,此时把aux[j-l]的数据直接放在arr[k]中
        if( i > mid ){
                arr[k] = aux[j-l];
                j++;
            }
        else if( j > r ){
                arr[k] = aux[i-l];
                i++;
            }
    //确认i,j索引的合法性后,可以正常地进行归并操作
        else if( aux[i-l] < aux[j-l] ){
                arr[k] = aux[i-l];
                i ++;
            }
        else{
                arr[k] = aux[j-l];
                j ++;
            }
    }
}

优化1:

提前判断是否有序

template<typename T>
void __mergeSort(T arr[], int l, int r){
    //每个分组只剩下1个元素
    if( l >= r )
        return;
    //二分分组
    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    //判断条件,否则已经有序
    if ( arr[mid] > arr[mid+1] )
        __merge(arr, l, mid, r);
}

优化2:

高级排序方法在n比较小的时候都可以用插入排序来代替,改变递归底层的条件

template<typename T>
void __mergeSort(T arr[], int l, int r){
    //每个分组只剩下1个元素
    /*if( l >= r )
        return;*/

    //当数据小于n时,使用插入排序
    if (r - l <= 15){
        insertionSort(arr, l, r);
        return;
    }
    //二分分组
    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    //判断条件,否则已经有序
    if ( arr[mid] > arr[mid+1] )
        __merge(arr, l, mid, r);

归并排序(自底向上的归并排序)

没有使用通过索引获取元素这一数组的特性,因此可以很好地使用o(nlogn)的时间复杂度处理链表的排序。

template <typename T>
void mergeSortBU(T arr[], int n){
    //原版排序
    for( int size = 1; size <= n ; size += size )
        //确保数组不越界
        for( int i = 0 ; i + size < n ; i += size + size )
            // 对 arr[i...i+size-1] 和 arr[i+size...i+2*size-1] 进行归并
            __merge(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );

    // Merge Sort Bottom Up 优化,对于小N,使用插入排序
    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 )
            //在非有序的情况下再进行排序
            if( arr[i+sz-1] > arr[i+sz] )
                __merge(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
}

快速排序

原版:

// 对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){

    T v = arr[l];

    int j = l; 
    // arr[l+1...j] < v ; arr[j+1...i) > v
    for( int i = l + 1 ; i <= r ; i ++ )
        //若arr[i] > v ,则直接i++即可,若arr[i] < v,则需要交换
        if( arr[i] < v ){
            j ++;
            swap( arr[j] , arr[i] );
        }
    swap( arr[l] , arr[j]);
    //返回元素v的索引j
    return j;
}

// 对arr[l...r]部分进行快速排序
template <typename T>
void __quickSort(T arr[], int l, int r){
    //递归结束条件
    if( l >= r )
        return;
    //p为中间元素v的索引
    int p = __partition(arr, l, r);
    __quickSort(arr, l, p-1 );
    __quickSort(arr, p+1, r);
}

template <typename T>
void quickSort(T arr[], int n){
    __quickSort(arr, 0, n-1);
}

优化1:

高级排序方法在n比较小的时候都可以用插入排序来代替,改变递归底层的条件

template <typename T>
void __quickSort(T arr[], int l, int r){
    //递归结束条件
    //if( l >= r )
    //    return;
    //当n比较小的时候,可以用插入排序来代替
    if(r - l <= 15){
      insetionSort(arr,l,r);
    }
    //p为中间元素v的索引
    int p = __partition(arr, l, r);
    __quickSort(arr, l, p-1 );
    __quickSort(arr, p+1, r);
}

优化2:

不再把第一个数作为标志位,而是随机选取一个数作为标志位

template <typename T>
void quickSort(T arr[], int n){
    srand(time(NULL));
    __quickSort(arr, 0, n-1);
}

// 对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){
    //rand()%(r - l + 1) + l  随机在[l,r]中选取一个索引
    swap(arr[l] , arr[rand()%(r - l + 1) + l]);
    T v = arr[l];

    int j = l; 
    // arr[l+1...j] < v ; arr[j+1...i) > v
    for( int i = l + 1 ; i <= r ; i ++ )
        //若arr[i] > v ,则直接i++即可,若arr[i] < v,则需要交换
        if( arr[i] < v ){
            j ++;
            swap( arr[j] , arr[i] );
        }
    swap( arr[l] , arr[j]);
    //返回元素v的索引j
    return j;
}

优化3:

使用i,j索引分别从序列两端开始搜索,小于v的在左边,大于v的在右边。这样分出来的两部分序列的长度差异不会太大。

// 对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
template <typename T>
int __partition2(T arr[], int l, int r){
    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){
        while(i <= r && arr[i] < v)  i++;
        while(j >= l+1 && arr[j] > v) j--;
        if(i > j) break;
        swap(arr[i], arr[j]);
        i++;
        j--; 
    }
    //此时索引j指向从左往右看,最后一个小于v的元素,而此时索引l在小于v的部分,因此需要交换l和j位置的元素
    swap(arr[l], arr[j]);
    return j;
}

// 对arr[l...r]部分进行快速排序
template <typename T>
void __quickSort2(T arr[], int l, int r){
    //递归结束条件
    //if( l >= r )
    //    return;
    if(r - l <= 15){
        insertionSort(arr,l,r);
        return;
    }
    //p为中间元素v的索引
    int p = __partition(arr, l, r);
    __quickSort2(arr, l, p-1 );
    __quickSort2(arr, p+1, r);
}

template <typename T>
void quickSort2(T arr[], int n){
    srand(time(NULL))
    __quickSort2(arr, 0, n-1);
}

优化4:Quick Sort 3 Ways

对于有大量重复键值的排序的优化。分为<v (arr[l + 1…lt])=v(arr[lt + 1…i-1]) >v(arr[gt…r])三部分

//三路快速排序处理 arr[l..r]
//将arr[l..r]分为 <v; ==v; >v 三部分
template <typename T>
void __quickSort3Ways(T arr[], int l, int r){

    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }

    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] );
    //此时lt指向=v的第一个元素,因此需要对[l,lt-1]进行快排
    __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 和Qucik Sort的衍生问题

这两种排序都使用了分治算法的思想。

  • 逆序对问题
    1.Merge Sort

  • 取出一个数组中第n大的元素

    1.Quick Sort(o(n)).
    思路:标志位的元素v排好序后在数组中的位置,就是其最终位置。eg:元素v=4在一次Quick Sort过后排数组的第4位,则其最后在数组中的位置就是4.此时我们想找第6大的元素时,只需要再对>4的部分递归一次Quick Sort,并找出其中第二位置(4+2=6)的元素即可.


堆排序(Heap Sort)

  • 优先队列(适合动态数据的维护)
  • 在N个元素中选出前M个元素

使用排序+提取 复杂度为NlogN

使用优先队列 复杂度为NlogM

  • 优先队列的实现

使用的数据结构来实现

二叉堆(最大堆)
  • 任何一个节点k都不大于其父节点 k/2

  • 必须是一个完全二叉树(除了最后一层外,其余层的节点数都为最大值,且最后一层的节点都集中在同一侧)

使用数组存储二叉堆
  • 数组索引从1开始

  • Shift Up 在堆中添加元素。 不断地交换该节点和其父节点的元素,以维持最大堆的特性

    template<typename Item>
    class MaxHeap{
    
    private:
      Item *data;
      int count;
      int capacity;
    
      void shiftUp(int k){
          while( k > 1 && data[k/2] < data[k] ){
              swap( data[k/2], data[k] );
              k /= 2;
          }
      }
    
    public:
    
      MaxHeap(int capacity){
          data = new Item[capacity+1];
          count = 0;
          this->capacity = capacity;
      }
    
      ~MaxHeap(){
          delete[] data;
      }
    
      int size(){
          return count;
      }
    
      bool isEmpty(){
          return count == 0;
      }
    
      void insert(Item item){
          assert( count + 1 <= capacity );
          data[count+1] = item;
          count ++;
          shiftUp(count);
      }
  • Shift Down 取出堆中最大的元素。1.将最后一个元素放在索引1处,以维持最大二叉树的性质;2.进行Shift Down操作,以维持 其最大堆的性质。(左右孩子节点,哪个大跟哪个交换

    template<typename Item>
    class MaxHeap{
    
    private:
      Item *data;
      int count;
      int capacity;
    
      void shiftUp(int k){
          while( k > 1 && data[k/2] < data[k] ){
              swap( data[k/2], data[k] );
              k /= 2;
          }
      }
    
      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;
          }
      }
    
    public:
    
      MaxHeap(int capacity){
          data = new Item[capacity+1];
          count = 0;
          this->capacity = capacity;
      }
    
      ~MaxHeap(){
          delete[] data;
      }
    
      int size(){
          return count;
      }
    
      bool isEmpty(){
          return count == 0;
      }
    
      void insert(Item item){
          assert( count + 1 <= capacity );
          data[count+1] = item;
          shiftUp(count+1);
          count ++;
      }
    
      Item extractMax(){
          assert( count > 0 );
          Item ret = data[1];
    
          swap( data[1] , data[count] );
          count --;
          shiftDown(1);
    
          return ret;
      }
    
      Item getMax(){
          assert( count > 0 );
          return data[1];
      }
    
    };

    可以进行进一步的优化,把交换(swap)过程用赋值来替换。

堆排序
  • 原版

    MaxHeap(int capacity){
          data = new Item[capacity+1];
          count = 0;
          this->capacity = capacity;
      }
    
    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();
    
    }
    
  • 改进1

    1.Heapify
    改进建堆的方式,直接创建一个具有最大堆性质的数组【复杂度为O(n)】,而不是通过maxheap.insert来一个个插入空堆【复杂度为O(nlogn)】。
    从第一个不是叶子节点处(索引为n/2)开始进行ShiftDown操作,因为最后一层的所有叶子节点都分别已经是一个最大堆了(每个堆只有一个元素).

    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;
          //从n/2处开始ShiftDown操作
          for( int i = count/2 ; i >= 1 ; i -- )
              shiftDown(i);
      }
    
    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();
    
    }
  • 改进2:原地堆排序(不需要额外的空间)
    1.将数组进行Heapify操作,将其变成Max Heap;
    2.取出最大值元素,将(最大值)与最后一个元素交换位置;
    3.将目前的第一个元素进行ShiftDown操作,此时整个数组又称为了Max Heap.

    4.需要注意的是,原地堆排序直接在原数组上进行,因此索引是从0开始的。此时父节点左右叶子节点变为

    parent(i) = (i-1) / 2
    left child (i) = 2 * i + 1
    right child (i) = 2 * i + 2

排序算法总结

  • 排序算法的稳定性
    对于相等的元素,在排序后,原来靠前的元素依然靠前。相等元素的相对位置没有发生变化。
    插入排序归并排序是稳定的

索引堆(Index Heap)

data不变,将Index进行Heap操作。比较的时候还是用data,交换的时候只交换index
只需要把之前Shift Up/Shift Down操作中的比较换成 data[index[k/2]]即可

索引堆的反向查找(reverse)
  • 在data、index的基础上再维护一个reverse数组。reverse[i]表示索引i在index(堆)中的位置,且有如下性质:
index[i] = j
reverse[j] = i

index[reverse[i]] = i
reverse[index[i]] = i

在每次交换后index后,都需要利用以上性质维护reverse数组 。

二叉搜索树(Binary Search Tree)

  • 可以用来解决查找问题(Searching Problem)

二分查找法(o(logn))

  • 对于有序数列,才能使用二分查找法
//在有序数组arr中查找元素target
//如果找到target,则返回其索引index
//否则返回-1
template<typename T>
int binarySearch(T arr[], int n, T target){
  //在arr[l...r]中查找target
  int l = 0, r = n -1;
  while( l <= r){
    int mid = l + (r - l) / 2;//不使用mid = (r + l) / 2的原因是防止过大的int溢出
    if(target == mid){
      return mid;
    }
    if(target < arr[mid]){
      //arr[l...mid - 1]
      r = mid - 1;
    }
    else
      //arr[mid + 1...r]
      l = mid + 1;
  }
  return -1;//未找到target
}

// 用递归的方式写二分查找法
template<typename T>
int __binarySearch2(T arr[], int l, int r, T target){

    if( l > r )
        return -1;

    //int mid = (l+r)/2;
    //防止极端情况下的整形溢出,使用下面的逻辑求出mid
    int mid = l + (r - l) / 2;

    if( arr[mid] == target )
        return mid;
    else if( arr[mid] > target )
        return __binarySearch2(arr, l, mid-1, target);
    else
        return __binarySearch2(arr, mid+1, r, target);
}

template<typename T>
int binarySearch2(T arr[], int n, T target){

    return __binarySearch2( arr , 0 , n-1, target);
}
floor & ceil函数
  • 数组中有大量重复元素。floor(元素第一次出现的位置)、ceil(元素最后一次出现的位置)
// 二分查找法, 在有序数组arr中, 查找target
// 如果找到target, 返回第一个target相应的索引index
// 如果没有找到target, 返回比target小的最大值相应的索引, 如果这个最大值有多个, 返回最大索引
// 如果这个target比整个数组的最小元素值还要小, 则不存在这个target的floor值, 返回-1
template<typename T>
int floor(T arr[], int n, T target){

    assert( n >= 0 );

    // 寻找比target小的最大索引
    int l = -1, r = n-1;
    while( l < r ){
        // 使用向上取整避免死循环
        int mid = l + (r-l+1)/2;
        if( arr[mid] >= target )
            r = mid - 1;
        else
            l = mid;
    }

    assert( l == r );

    // 如果该索引+1就是target本身, 该索引+1即为返回值
    if( l + 1 < n && arr[l+1] == target )
        return l + 1;

    // 否则, 该索引即为返回值
    return l;
}


// 二分查找法, 在有序数组arr中, 查找target
// 如果找到target, 返回最后一个target相应的索引index
// 如果没有找到target, 返回比target大的最小值相应的索引, 如果这个最小值有多个, 返回最小的索引
// 如果这个target比整个数组的最大元素值还要大, 则不存在这个target的ceil值, 返回整个数组元素个数n
template<typename T>
int ceil(T arr[], int n, T target){

    assert( n >= 0 );

    // 寻找比target大的最小索引值
    int l = 0, r = n;
    while( l < r ){
        // 使用普通的向下取整即可避免死循环
        int mid = l + (r-l)/2;
        if( arr[mid] <= target )
            l = mid + 1;
        else // arr[mid] > target
            r = mid;
    }

    assert( l == r );

    // 如果该索引-1就是target本身, 该索引-1即为返回值
    if( r - 1 >= 0 && arr[r-1] == target )
        return r-1;

    // 否则, 该索引即为返回值
    return r;
}

二分搜索树

  • 优势:查找表的实现——字典数据结构
  • 不仅可以查找数据,还可以高校地插入、删除数据(动态维护数据)

1.首先是一颗二叉树

2.每个节点的value都大于左孩子/左子树,且小于右孩子/右子树

3.以左右孩子为根的子树仍为二分搜索树

4.天然的递归特性

5.不一定是完全二叉树

在二分搜索树中插入节点(insert)
//向二分搜索树中插入一个新的(key,value)数据对
void insert(Key key, Value value){
    root = insert(root, key, value);
}

private:
    //向以node为根的二分搜索树中,插入节点(key,value),使用递归算法
    //返回插入新节点后的二分搜索树的根
    Node* insert(Node *node, Key key, Value value){
        //不存在左孩子或右孩子节点,则把自己作为根节点
        if(node == NULL){
            count ++;
            return new Node(key, value);
        }

        if(key == node->key)
            node-value = value;
        else if(key < node->key)
            node->left = insert(node->left, key, value);
        else //key > node->key
            node->right = insert(node->right, key, value);

        return node;
    }
二分查找树的查找

和插入操作的原理差不多。
要查找树中是否包含某键值为key的元素,可以使用contain

bool contain(Key key){
    return contain(root, key);
}

//查看以node为根的二叉搜索树中是否包含键值为key的节点
bool contain(Node* node, Key key){

    if(node == NULL)
        return false;

    if(key == node->key)
        return true;
    else if(key < node->key)
        return contain(node->left, key);
    else return contain(node->right, key);

search()
返回值可以是Node节点,但是封装性能不好;
返回值可以是Value,但是使用前必须保证整个树是contain这个元素的;若不存在则无法返回
返回值可以是Value*,若元素不存在,则可以返回空指针,用户也可以方便地修改元素。

Value* search(Key, key){
    return search(root, key);

//在以node为根的二叉搜索树中查找key所对应的value
Value* serach(Node* node, Key key){

    if(node == NULL)
        return NULL;

    if( key == node->key)
        //返回指针
        return &(node->value);
    else if(key < node->key)
        return search(node->left, key);
    else
        return search(node->right, key);
}
二分搜索树的遍历(深度优先遍历)

前序:先访问当前节点,再依次递归访问左右子树
中序:先递归访问左子树,再访问自身,再递归访问右子树(输出内容是从小到大的,可以用做排序
后序:先递归访问左右子树,再访问自身节点(可以用于销毁二叉搜索树

    // 对以node为根的二叉搜索树进行前序遍历, 递归算法
    void preOrder(Node* node){

        if( node != NULL ){
            cout<<node->key<<endl;
            preOrder(node->left);
            preOrder(node->right);
        }
    }

    // 对以node为根的二叉搜索树进行中序遍历, 递归算法
    void inOrder(Node* node){

        if( node != NULL ){
            inOrder(node->left);
            cout<<node->key<<endl;
            inOrder(node->right);
        }
    }

    // 对以node为根的二叉搜索树进行后序遍历, 递归算法
    void postOrder(Node* node){

        if( node != NULL ){
            postOrder(node->left);
            postOrder(node->right);
            cout<<node->key<<endl;
        }
    }
层序遍历(广度优先遍历)

使用队列实现广度优先遍历
1.根节点入队->出队
2.出队节点的左、右子节点入队
3.队首出队->重复2.

void levelOrder(){
    queue<Node*> q;
    q.push(root);
    while( !q.empty*() ){
        Node* node = q.front();
        q.pop();//队首出队

        cout<<node->key<<endl;
        //出队节点的左、右子节点入队
        if(node->left)
            q.push(node->left);
        if(node->right)
            q.push(node->right);
二叉搜索树删除节点 O(logn)
  • 删除最小值和最大值
    由于二分搜索树的性质,在删除最大值或最小值后,可以直接将其子节点放在其原来的位置即可。
//删除最小值节点
Void removeMin(){
    if( root )
        root = removeMin(root);

Node* removeMin(Node* node){
    //本身已是最小节点
    if(node ->left == NULL){
        Node* rightNode = node->right;
        delete node;
        count --;
        reutrn rightNode;
    }
    //以上是删除过程

    node->left = removeMin(node->left);
    return node; 

//删除最大值节点    
Void removeMax(){
    if( root )
        root = removeMax(root);

Node* removeMax(Node* node){
    //本身已是最大节点
    if(node ->right == NULL){
        Node* leftNode = node->left;
        delete node;
        count --;
        reutrn leftNode;
    }
    //以上是删除过程

    node->right = removeMax(node->right);
    return node; 
  • 二分搜索树删除任意节点
    删除只有左孩子或只有右孩子的节点,和删除最大值、最小值节点的操作是一致的。
    删除左右都有孩子的节点:Hubbard Deletion算法
    1.找到删除节点d 右子树中的最小值 s = min(d->right)
    2.s->right = removeMin(d->right) 此处删除+连接过程写在了一个语句里面
    3.s->left = d->left
    4.delete d
void remove(Key key){
    root = remove(root,key);
}

Node* remove(Node* node, Key key){
    if( node == NULL )
        return NULL;
    if( key < node->key ){
        node->left = remove( node->left, key);
        return node;
    }
    else if( key > node->key ){
        node->right = remove( node->right, key);
        return node;
    }
    else{ //key == node->key

        //删除只有右孩子的节点,等同于删除最小值节点
        if( node->left == NULL){
            Node *rightNode = node->right;
            delete node;
            count --;
            return rightNode;
        }
        //删除只有左孩子的节点,等同于删除最大值节点
        if( node->right == NULL){
            Node *leftNode = node->left;
            delete node;
            count --;
            return leftNode;
        }
        //删除左右孩子都不为空的节点 Hubbard Deletion
        Node *successor = new Node(minimum(node->right));
        count ++;

        successor->right = removeMin(node->right);
        successor->left = node->left;

        delete node;
        count --;

        return successor;
    }
}

同样也可以:
1.找到删除节点d 左子树中的最大值 s = max(d->left)
2.s->left = removeMax(d->left)
3.s->right = d->right
4.删除d

二分搜索树的顺序性

1.Maximum、Minimum
2.Successor(右子树的最小值)、Predecessor(左子树的最大值)
3.floor、ceil
4.rank 元素s排第几?
5.select 排名第s的元素是谁?

二分搜索树的局限性

二分搜索树可能退化成链表,一种解决方法:
平衡二叉树:红黑树
平衡二叉树和堆的结合:Treap

并查集Union Find

一种不一样的树形结构

  • 可以解决连接问题
    网络中节点间的连接状态
  • 数学中的集合类实现

并查集的实现

union(p,q)——>并
find(p)——>查p的根节点
isConnected(p,q)

  • Qucik Union
    每个节点都存储父亲元素的信息,若一个节点已为根节点,则其父亲元素为自己
    保证两个元素的根节点是连接的
int find(int p){
    assert(p >= 0 && p < count );
    // 不断去查询自己的父亲节点, 直到到达根节点
    // 根节点的特点: parent[p] == p
    while( p != parent[p] )
        p = parent[p];
    return p;
}

void unionElements(int p, int q){
    int pRoot = find(p);
    int qRoot = find(q);
    if( pRoot == qRoot )
        return;
    parent[pRoot] = qRoot;
}
  • 并查集的优化1(Optimize by Size)
    每次Union都将元素少的集合的根节点指向元素多的集合的根节点(减小整个树的高度,从而减小复杂度)
void unionElements(int p, int q){
    int pRoot = find(p);
    int qRoot = find(q);
    if( pRoot == qRoot )
        return;

    if (sz[pRoot] < sz[qRoot] ){
        parent[pRoot] = qRoot;
        sz[qRoot] += sz[pRoot];//集合元素的个数
    }
    else{
        parent[qRoot] = pRoot;
        sz[pRoot] += sz[qRoot];
    }
}
  • 并查集的优化2(Optimize by Rank)
    rank[ ]:集合所表示的树的层数
void unionElements(int p, int q){
    int pRoot = find(p);
    int qRoot = find(q);
    if( pRoot == qRoot )
        return;

    if (rank[pRoot] < rank[qRoot] ){
        parent[pRoot] = qRoot;
    }
    else{
        parent[qRoot] = pRoot;
    }
    else{// rank[pRoot] = rank[qRoot]
        parent[pRoot] = qRoot; 
        rank[qRoot] += 1;
    }
}
  • 并查集的优化3(路径压缩)
int find(int p){
    assert(p >= 0 && p < count );
    while( p != parent[p] ){
        // 路径压缩
        parent[p] = parent[parent[p]];
        p = parent[p]; 
    } 
    return p;
} 

图论基础

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值