Algorithm Clearance Village シリーズの記事のディレクトリ
記事ディレクトリ
序文
本系列文章是针对于鱼皮知识星球——编程导航中的算法通关村中的算法进行归纳和总结。 该篇文章讲解的是第三关中的白银挑战———双指针思想
配列や文字列などの要素が格納される連続空間において、後続の要素が前に移動したい場合、後続の要素の連続性を保つためには、後続の要素全体が移動する必要があります。要素は全体として後方に移動する必要があるため、一部のアルゴリズムでは複数のラウンドが必要になります。多数の要素を移動すると効率が低下します。この問題を解決するには、次のメソッドを見てみましょう。 this - ダブルポインタ
1. ダブルポインタの概要
ダブル ポインタは、実際にはポインタである必要はありません。多くの場合、これらは 2 つの変数です。操作する要素を指すために 2 つの変数を使用することが多いため、変数を単にダブル ポインタと呼びます。ダブルポインタと呼ばれるので、ポインタは2つあるはずです。ダブル ポインタでは、ポインタの方向によってさまざまな状況が考えられます。たとえば、両方のポインタが最初から始まります。この状況は高速ダブル ポインタと低速ダブル ポインタ; 2 つのポインターが配列の両端から始まり、配列の内側に向かう場合、この状況は二重ポインターの衝突と呼ばれます。
ダブル ポインタの種類に関係なく、重要なのは、ポインタの前後に修飾された要素があるということです。ポインタは配列要素を横断し、いくつかの変更を通じてこれらの要素を変更します。修飾された要素、修飾された要素を識別するポインタが移動され、これらすべての修飾された要素がその「ルール名」に分割されます。最後の 2 つのポインタが一致するということは、配列内のすべての要素が修飾された要素になったことを意味します。
次に、LeetCode の質問を通じてダブル ポインターの内容を分析します。
2. val に等しい配列要素をすべて削除します。
LeetCode 27 の質問:
配列 nums と値 val が与えられた場合、値が val に等しいすべての要素を削除し、その後の配列の新しい長さを返す必要があります。取り外しです。
余分な配列スペースを使用しないでください。O(1) 個の余分なスペースのみを使用し、入力配列をインプレースで変更する必要があります。
要素の順序は変更できます。新しい長さを超える配列内の要素を考慮する必要はありません。
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
この質問に対する 2 つのダブル ポインターの使用法を分析してみましょう
2.1 高速ダブル ポインタと低速ダブル ポインタ
序文で、ダブル ポインタの種類に関係なく、条件を満たす要素をマークするポインタと、要素を横断して条件を満たさない要素を作成するポインタが存在することを紹介しました。条件が条件を満たしている場合は、該当する要素の数を増やします。
高速および低速のデュアル ポインタでは、高速ポインタを高速ポインタ、低速ポインタをスローと呼びます。そして、低速ポインタは条件を満たす要素をマークするために使用され、高速ポインタはトラバースするために使用されます
ステップ:
- low、fastともに配列の先頭要素を指す0に初期化されますが、ここでの配列は昇順や降順などの特別な配列ではないため、先頭要素が条件を満たすかどうか判断できないため、代入されます。値 0 を直接指定します。
- Fast は配列を走査する必要があります。要素を走査するたびに判定する必要があります。この問題は、値 val を持つ要素を削除することです。 fast が要素が条件を満たしていると判断した場合、それは問題ではありません。slow の意味は、slow-1 より前の要素がすべて条件を満たしており、要素がslowが指す要素が処理対象の要素で、fastが判定し、fastは0が条件を満たしていると判断し、slowが指す要素に0を代入することでslowは1ビット進むことができます。
- fast が条件を満たしていないと判断した場合、slow ポインタは移動しませんが、結局、この要素は条件を満たさないため、含めるべきではありません。 fast ポインタは、条件を満たす要素に移動するまで移動し続け、その要素の値を使用して、slow が指す値を上書きし、slow が指す値が条件を満たしてから、slow が移動できるようになります。
public static int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) nums[slow++] = nums[fast];
}
return slow;
}
2.2 ダブルポインタの衝突
速いダブル ポインタと遅いダブル ポインタが同じ方向に移動することを確認したので、異なる方向に衝突するダブル ポインタを見てみましょう。衝突するダブル ポインタでは、両側が同時に移動する必要があります。
ステップ:
- 左ポインタは 0 から始まり、右ポインタは配列の末尾から始まり、左ポインタの前の要素は修飾された要素、右ポインタの後の要素はすべて非修飾要素です。
- left が指す要素が val と等しくなく、条件を満たす場合、left は 1 位置後方に移動します。
- right が指す要素が val に等しく条件を満たさない場合、right は 1 つ前に移動します。
- 左の指す要素が val と等しい場合は条件を満たさず、右の指す要素が val と等しくない場合は条件を満たします このとき、左と右の指す要素は左の左側が条件を満たし、右の右側が条件を満たさないように交換されます。
コード:
public static int removeElementCash(int[] nums, int val) {
int left = 0;
int right = nums.length - 1;
for (left = 0; left <= right; ) {
// 没找到
if ((nums[left] == val) && (nums[right] != val)) {
// 这里一定是交换值,而不是简单的把nums[right]赋值给nums[left],因为当right检测到一个不是val的值时
// 如果不交换,right就一直停留在这个值,不会继续向前检索
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
if (nums[left] != val) left++;
if (nums[right] == val) right--;
}
return left;
}
3. 要素の奇数偶数移動
LeetCode 905
整数配列 nums を指定して、nums 内のすべての偶数要素を配列の先頭に移動し、その後にすべての奇数要素を移動します。
この条件を満たす配列を答えとして返します。
入力: nums = [3,1,2,4]
出力: [2,4,3,1]
説明: [ 4,2,3,1]、[2,4,1,3]、および [4,2,1,3] も正解とみなされます。
このタイプは 2 つのタイプに分かれており、衝突するダブル ポインタを記述するのに非常に適しており、書き込み方法は上記の衝突するダブル ポインタと同様です。ここでは分析せず、コードを直接投稿します。
public static int[] sortArrayByParity(int[] nums){
int left=0;
int right=nums.length-1;
for (left=0;left<=right;){
if((nums[left]%2!=0&&nums[right]%2==0)){
int temp=nums[right];
nums[right]=nums[left];
nums[left]=temp;
}
if(nums[left]%2==0) left++;
if(nums[right]%2!=0) right--;
}
return nums;
}
4. 配列の間隔問題
LeetCode 228:
重複要素のない順序付き整数配列 nums が与えられます。
配列内のすべての数値を正確にカバーする最小の順序付き範囲リストを返します。つまり、 nums のすべての要素は、正確に一定の範囲範囲に含まれており、範囲に属して nums に属さない数値 x は存在しません。
入力: nums = [0,1,2,4,5,7]
出力: ["0->2", "4->5", "7"]
説明: 間隔の範囲は:
[0,2] --> "0->2"< a i =4> [4,5] -->「4->5」 [7,7] -->「7」
この問題を解決するために、高速ダブル ポインタと低速ダブル ポインタを使用します。
ステップ:
- low と mi の間の要素は修飾されており、fast は条件を満たす要素を見つける責任を負います。これは、slow が特定の間隔の左端点であり、mi が特定の間隔の右端点であることに相当します。
- mi + 1 が指す要素と fast が指す要素が等しくない場合、つまりこの時点では mi と fast が連続していないため条件を満たさないため、slow と fast の間の要素は選んで、ゆっくりさせてください。今、ミとミはどちらも速いに等しいです。
- 最後の要素を高速移動すると、必ず取り出されていない要素または範囲が残りますので、この時点で仕上げ作業が必要になります。
スリーポインターコード:
public List<String> summaryRanges(int[] nums) {
List<String> strings = new ArrayList<>();
if(nums.length==0) return strings ;
int slow=0;
int fast=0;
int mi=0;
for ( fast = 1; fast < nums.length; fast++) {
if(nums[mi]+1!=nums[fast]){
if (nums[slow]==nums[mi]) {
strings.add(nums[slow]+"");
}else {
strings.add(nums[slow]+"->"+nums[mi]);
}
slow=fast;
mi=fast;
}else {
mi++;
}
}
if(slow!= nums.length-1) strings.add(nums[slow]+"->"+nums[nums.length-1]);
if(slow== nums.length-1) strings.add(nums[nums.length-1]+"");
return strings;
}
ここで調べてみたところ、fast で mi もできることが分かりました。ここでは fast を mi、fast+1 を本来の fast とみなします。slow と fast の間の要素も使用できます。条件を満たしているので、より明確です
ダブル ポインター コード:
public static List<String> summaryRanges(int[] nums){
List<String> strings = new ArrayList<>();
int slow=0;
int fast=0;
// int mi=0;
// 这个fast既起到了我们的mi又相当于fast
for ( fast = 0; fast < nums.length; fast++) {
if(fast+1== nums.length||nums[fast]+1!=nums[fast+1]){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(nums[slow]);
if (nums[slow]!=nums[fast]) {
stringBuilder.append("->"+nums[fast]);
}
strings.add(stringBuilder.toString());
slow=fast+1;
// mi=fast;
}
// else {
// mi++;
// }
}
// if(slow!= nums.length-1) strings.add(nums[slow]+"->"+nums[nums.length-1]);
// if(slow== nums.length-1) strings.add(nums[nums.length-1]+"");
return strings;
}
要約する
ダブル ポインタの単純な使用はこれで終わりです。本質は、ポインタが修飾された要素を分割し、ポインタが修飾された要素を検索するということです。これは比較的理解しやすいです。このレベルの黄金の課題を後でリストします。たとえば、ダブル ポインターに関するいくつかの難しい質問もありますが、考え方は同じです。処理中に考慮すべき点が増えただけです。慎重に検討した後は、接続することができます。