慕課網-演算法與數據結構-學習總結
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;
}
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;
}
}
// 使用優化的歸併排序演算法, 對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);
}
核心程式碼參考
```
// 使用自底向上的歸併排序演算法
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就是一個反例
// 關於這部分陷阱,推薦看(liubobo老師)的《玩轉演算法面試》課程,第二章:《面試中的複雜度分析》:)
}
```
基本思想: (分治) 將選定的元素放到合適的位置,然後 遞回 排序 被該元素分開的 前 後 兩個子序列。(分出來的兩個子序列可能不等長,相差很大,會影響效能)
核心程式碼參考
```
// 對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);
}
三路快排
// 遞回的三路快速排序演算法
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
- 基本思想:從最後一個非葉子節點(索引爲count/2,在根節點索引從1開始的情況下)開始,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);
}
```
- 程式碼
```
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;
}
}
```
* 堆排序演算法:
* 建立堆: 一種通過不斷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操作*
// 索引堆中, 數據之間的比較根據data的大小進行比較, 但實際操作的是索引
void shiftUp( int k ){
while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
swap( indexes[k/2] , indexes[k] );
k /= 2;
}
}
// 索引堆中, 數據之間的比較根據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;
}
- 多路歸併排序 多個元素同時比較的時候用,最小(大)堆
- 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) | 否 |
其他參考
C語言中的14種排序
14中排序動畫演示
二分搜尋樹的定義:
二分搜尋樹的優勢
二分搜尋樹的節點插入
// 向以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;
}
二分搜尋書的查詢
二分搜尋樹的遍歷
刪除最大值,最小值
刪除Key對應的節點
順序性
侷限性:(不平衡) 依照順序或逆序插入元素,二分搜尋樹退化爲鏈表(跟節點爲第一個元素,在最左,或最右)。
支援重複鍵值的二分搜尋樹
- 平衡二元樹:
- 左右子樹高度差不超過1的二元搜尋樹,即平衡二元樹所有結點的平衡因子絕對值不超過1(平衡因子 = 結點左子樹的高度 - 結點右子樹的高度)。
- 實現方式:
- AVL樹
- 紅黑樹
- 2-3樹
- Splay樹
基本思想,每插入一個節點,自底向上維護樹的平衡,以及每個節點的平衡因子
平衡調整的四種類型:
插入操作思路整理:
左平衡調整(cur.bf > 1)
右平衡調整(cur.bf < -1)
插入操作程式碼
//插入操作
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;
}
多路查詢樹的意義
對於樹來說,一個結點只能儲存一個元素,那麼在元素非常多的時候,就會使得要麼樹的度非常大(結點擁有子樹的個數的最大值),要麼樹的高度非常大,甚至兩者都必須足夠大才行,這就使得記憶體存取外存次數非常多,這顯然成了時間效率上的瓶頸,這迫使我們要打破每一個結點只儲存一個元素的限制,爲此引入了多路查詢樹的概念
2-3 樹
2-3-4 樹
B 樹
// 查詢過程, 查詢元素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];
}