算法与数据结构
标签(空格分隔): 数据结构 算法
复杂度为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;
}