生活随笔
收集整理的這篇文章主要介紹了
two pointers
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
目錄
什么是two pointers
two pointers是算法編程中一種非常重要的思想,但是很少會有教材單獨拿出來講,其中一個原因是它更傾向于是一種編程技巧,而長得不太像是一個“算法”的模樣。two pointers的思想十分簡潔,但卻提供了非常高的算法效率,下面就來一探究竟。 以一個例子引入: 給定一個遞增的正整數序列和一個正整數M,求序列中的兩個不同位置的數a和b, 使得它們的和恰好為M,輸出所有滿足條件的方案 。 例如給定序列{1,2, 3,4,5, 6)和正整數M=8,就存在2+6=8與3+5=8成立。 本題的一個最直觀的想法是,使用二重循環枚舉序列中的整數a和b,判斷它們的和是否為M, 如果是,輸出方案;如果不是,則繼續枚舉。 代碼如下:
for ( int i
= 0 ; i
< n
; i
++ )
{ for ( int j
= i
+ 1 ; j
< n
; j
++ ) { if ( a
[ i
] + a
[ j
] == M
) { printf ( "%d %d\n" , a
[ i
] , a
[ j
] ) ; } }
}
顯然,這種做法的時間復雜度為O(n2 ),對n在105 的規模時是不可承受的。 來看看高復雜度的原因是什么:
對一個確定的 a[i] 來說,如果當前的 a[ j ] 滿足 a[ i ] + a[ j ] > M ,顯然也會有a[ i ] + a [ j + 1 ] > M 成立 (這是由于序列是遞增的),因此就不需要對a[ j ] 之后的數進行枚舉。如果無視這個性質,就會導致 j 進行了大量的無效枚舉, 效率自然十分低下。 對某個a[ i ]來說,如果找到一個a[j],使得a[ i ]+ a[ j ]> M恰好成立,那么對a[ i+1]來說,一定也有a[ i+1 ] + a[ j ]> M成立,因此在a[ i ]之后的元素也不必再去枚舉。
上面兩點似乎體現了一個問題: i 和 j 的枚舉似乎是互相牽制的,而這似乎可以給優化算法帶來很大的空間。事實上,本題中two pointers將利用有序序列的枚舉特性來有效降低復雜度。 它針對本題的算法過程如下: 令下標 i 的初值為0,下標 j 的初值為 n-1,即令i、j分別指向序列的第一個元素和最后一個元素, 接下來根據a[ i ] + a[ j ]與M的大小來進行下面三種選擇,使 i 不斷向右移動、使 j 不斷向左移動,直到i≥j成立,如圖4-8所示。 ①如果滿足a[ i ]+a[ j ]=M,說明找到了其中一組方案。 由于序列遞增,不等式a[ i+1]+a[ j ]>M與a[ i ]+a[ j- 1]< M 均成立, 但是a[ i+ 1]+a[ j- 1]與M的大小未知,因此剩余的方案只可能在[ i+ 1,j- 1]區間內產生, 令 i=i+1、j = j -1 (即令i向右移動,j向左移動)。
②如果滿足a[ i ] +a[ j ]> M,由于序列遞增,不等式a[ i+ 1 ]+a[ j ]>M成立,但是a[ i ]+a[ j - 1]與M的大小未知, 因此剩余的方案只可能在[i,j- 1]區間內產生,令j=j-1 (即令j向左移動)。
③如果滿足a[i]+a[j]<M,由于序列遞增,不等式a[i]+a[j- 1]<M成立,但是a[i+1]+ a[j]與M的大小未知, 因此剩余的方案只可能在[i+ 1,j]區間內產生,令i=i+1 (即令i向右移動)。 反復執行上面三個判斷,直到i≥j成立。代碼如下:
while ( i
< j
)
{ if ( a
[ i
] + a
[ j
] == m
) { printf ( "%d %d\n" , i
, j
) ; i
++ ; j
-- ; } else if ( a
[ i
] + a
[ j
] < m
) { i
++ ; } else { j
-- ; }
}
再來看序列合并問題。假設有兩個遞增序列A與B,要求將它們合并為一個遞增序列C. 同樣的,可以設置兩個下標i和j,初值均為0,表示分別指向序列A的第一個元素和序列B的第一個元素,然后根據A[i]與B[j]的大小來決定哪一個放入序列C:
若A[ i ] < B[ j ],說明A[ i ]是當前序列A與序列B的剩余元素中最小的那個,因此把A[ i ]加入序列C中,并讓 i 加1 (即讓 i 右移一位). 若A[ i ] > B[ j ],說明B[ j ]是當前序列A與序列B的剩余元素中最小的那個,因此把B[ j ]加入序列C中,并讓 j 加1 (即讓 j 右移一位). 若A[i]==B[j],則任意選一個加入到序列C中,并讓對應的下標加1.
上面的分支操作直到i、j中的一個到達序列末端為止,然后將另一個序列的所有元素依次加入序列C中 代碼如下:
int merge ( int A
[ ] , int B
[ ] , int C
[ ] , int n
, int m
)
{ int i
= 0 , j
= 0 , index
= 0 ; while ( i
< n
&& j
< m
) { if ( A
[ i
] <= B
[ j
] ) { C
[ index
] = A
[ i
] ; i
++ ; } else { C
[ index
] = B
[ j
] ; j
++ ; } index
++ ; } while ( i
< n
) { C
[ index
] = A
[ i
] ; i
++ ; index
++ ; } while ( j
< n
) { C
[ index
] = B
[ j
] ; j
++ ; index
++ ; } return index
;
}
最后,一定有讀者問, two pointers到底是怎樣的一種思想? 事實上, two pointers最原始的含義就是針對本節第一個問題而言的, 而廣義上的two pointers則是利用問題本身與序列的特性,使用兩個下標i、j對序列進行掃描(可以同向掃描,也可以反向掃描), 以較低的復雜度(一般是0(n)的復雜度)解決問題。讀者在實際編程時要能夠有使用這種思想的意識。
歸并排序
const int maxn
= 100 ;
void merge ( int A
[ ] , int L1
, int R1
, int L2
, int R2
)
{ int i
= L1
; int j
= L2
; int temp
[ maxn
] , index
= 0 ; while ( i
<= R1
&& j
<= R2
) { if ( A
[ i
] <= A
[ j
] ) { temp
[ index
++ ] == A
[ i
++ ] ; } else { temp
[ index
++ ] = A
[ j
++ ] ; } } while ( L1
<= R1
) { temp
[ index
++ ] = A
[ i
++ ] ; } while ( L2
<= R2
) { temp
[ index
++ ] = A
[ j
++ ] ; } for ( i
= 0 ; i
< index
; i
++ ) { A
[ i
] = temp
[ i
] ; }
}
void mergeSort ( int A
[ ] , int left
, int right
)
{ if ( left
< right
) { int mid
= ( left
+ right
) / 2 ; mergeSort ( A
, left
, mid
) ; mergeSort ( A
, mid
+ 1 , right
) ; merge ( A
, left
, mid
, mid
+ 1 , right
) ; }
}
1.遞歸實現 2-路歸并排序的遞歸寫法非常簡單,只需要反復將當前區間[left, right]分為兩半, 對兩個子區間[left, mid]與[mid +1, right]分別遞歸進行歸并排序,然后將兩個已經有序的子區間合并為有序序列即可, 代碼如下,其中merge函數為上一節的代碼改編而來,請讀者注意與上一節代碼的對比:
const int maxn
= 100 ;
void merge ( int A
[ ] , int L1
, int R1
, int L2
, int R2
)
{ int i
= L1
; int j
= L2
; int temp
[ maxn
] , index
= 0 ; while ( i
<= R1
&& j
<= R2
) { if ( A
[ i
] <= A
[ j
] ) { temp
[ index
++ ] == A
[ i
++ ] ; } else { temp
[ index
++ ] = A
[ j
++ ] ; } } while ( L1
<= R1
) { temp
[ index
++ ] = A
[ i
++ ] ; } while ( L2
<= R2
) { temp
[ index
++ ] = A
[ j
++ ] ; } for ( i
= 0 ; i
< index
; i
++ ) { A
[ i
] = temp
[ i
] ; }
}
void mergeSort ( int A
[ ] , int left
, int right
)
{ if ( left
< right
) { int mid
= ( left
+ right
) / 2 ; mergeSort ( A
, left
, mid
) ; mergeSort ( A
, mid
+ 1 , right
) ; merge ( A
, left
, mid
, mid
+ 1 , right
) ; }
}
2.非遞歸實現
void mergeSort ( int A
[ ] )
{ for ( int step
= 2 ; step
/ 2 <= n
; step
* = 2 ) { for ( int i
= 1 ; i
<= n
; i
+ = step
) { int mid
= i
+ step
/ 2 - 1 ; if ( mid
+ 1 <= n
) { merge ( A
, i
, mid
, mid
+ 1 , min ( i
+ step
- 1 ) , n
) ) ; } } }
}
當然,如果題目中只要求給出歸并排序每一趟結束時的序列,那么完全可以使用sort()函數代替 merge函數(只要時間限制允許),如下所示:
void mergeSort ( int A
[ ] )
{ for ( int step
= 2 ; step
/ 2 <= n
; step
* = 2 ) { for ( int i
= 1 ; i
<= n
; i
+ = step
) { sort ( A
, i
, mid
, mid
+ 1 , min ( i
+ step
- 1 ) , n
) ; } }
}
快速排序
其實大致的思路就是選一個數作為一個支點,大于它的數都在它的右邊,小于它的數都在它的左邊。
思路: 例子:
#include <cstdio>
int Partition ( int A
[ ] , int left
, int right
)
{ int temp
= A
[ left
] ; while ( left
< right
) { while ( left
< right
&& A
[ right
] > temp
) right
-- ; A
[ left
] = A
[ right
] ; while ( left
< right
&& A
[ left
] < temp
) left
++ ; A
[ right
] = A
[ left
] ; } A
[ left
] = temp
; return left
;
}
接下來就可以正式實現快速排序算法了。 快速排序的思路是:
調整序列中的元素,使當前序列最左端的元素調整后滿足左側所有元素均不超過 該元素,右側元素均不大于該元素。 對該元素的左側和右側分別遞歸進行1的調整,直到當前調整區的長度不超過1。
快速排序的遞歸實現如下:
void quickSort ( int A
[ ] , int left
, int right
)
{ if ( left
< right
) { int pos
= Partition ( A
, left
, right
) ; quickSort ( A
, left
, pos
- 1 ) ; quickSort ( A
, pos
+ 1 , right
) ; }
}
需要知道的是: rand()函數只可以生成 [ 0, RAND_MAX ] 范圍內的整數。RAND_MAX是stdlib.h中的一個常數, 在不同系統環境中,該常數的值有所不同,這里的是32767。
int randPartition ( int A
[ ] , int left
, int right
)
{ int P
= ( round ( 1.0 * rand ( ) / RAND_MAX
* ( right
- left
) ) + left
) ; swap ( A
[ left
] , A
[ P
] ) ; int temp
= A
[ left
] ; while ( left
< right
) { while ( left
< right
&& A
[ right
] > temp
) right
-- ; A
[ left
] = A
[ right
] ; while ( left
< right
&& A
[ left
] < temp
) left
++ ; A
[ right
] = A
[ left
] ; } A
[ left
] = temp
; return left
;
}
總結
以上是生活随笔 為你收集整理的two pointers 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。