[Day1] 配列、704 個の二分探索、27 個の要素を削除
演習の問題はリコウから出題され、コードのランダム記録に従って順序が実行されます。
配列
配列は、連続したメモリ空間に格納された同じタイプのデータの集合です。
- 配列の添え字は 0 から始まります
- 配列メモリ空間のアドレスは連続(1次元、2次元とも連続)
- 配列の要素は削除できず、上書きのみが可能です
- 配列はランダムアクセスストレージ構造です
C++ を使用する場合は、ベクトルと配列の違いに注意する必要があります。ベクトルの基礎となる実装は配列です。厳密に言えば、ベクトルはコンテナであり、配列ではありません。
Vector は連続したメモリ空間を使用して要素を格納するシーケンシャル コンテナですが、そのメモリ空間のサイズは変更できます。
配列はシーケンシャルコンテナであり、要素の格納にも連続メモリ空間を使用しますが、そのメモリ空間は固定サイズであり、適用後に変更することはできません。
704 二分探索
二分探索の前提は、配列が順序付けされた配列であり、配列内に重複する要素がないことです。
要素が繰り返されると、二分検索メソッドによって返される要素の添字は一意ではなくなる可能性があるため、これらは二分検索メソッドを使用するための前提条件です。タイトルの説明が上記の条件を満たしていることがわかったら、それについて考えることができます。二分法を使用します。
二分法を記述する場合、通常、間隔には 2 つの定義があります。左閉と右閉 [左、右]、または左閉と右開 [左、右)です。
トピックリンク: 704 二分探索
トピック: n 個の要素とターゲット値 target を持つ順序付き (昇順) 整数配列 nums が与えられた場合、nums でターゲットを検索し、ターゲット値が存在する場合は添え字を返し、存在しない場合は -1 を返す関数を作成します。
バージョン 1 は左が閉じ、右が閉じています [左、右]
[left, right] 左辺と右辺の両方を取得できます。nums[m] は target と等しくないため、nums[m]>target の場合、right=m-1
// 版本一 左闭右闭即[left, right]
class Solution {
public:
int search(vector<int>& nums, int target) {
//vector好比是一个数组
int left=0;
int right=nums.size()-1; //左闭右闭,下标从0开始,最后一个数就是个数减1
while (left<=right)
{
int middle=left+((right-left)/2); //防止溢出,等价于 (left+right)/2
if(nums[middle]>target){
right=middle-1;//target在左区间, middle是几个数值的中间数 数组这里是以下标看来值,因此下标-1
}else if(nums[middle]<target){
left=middle+1;//target在右区间
}else{
//nums[middle]==target
return middle;
}
}
// 未找到目标值
return -1;
}
};
以下のコードについて
int middle = left + ((right - left) / 2); //而不用mid = (left+right)/2
理由:mid = (左 + 右) / 2 はオーバーフローしやすいです。left+right は int の範囲を簡単に超える可能性があるためです。また、mid = left + (right - left) / 2 はオーバーフローしにくいため、今後 2 進小数点を記述するときは、mid = left + (right - left) / 2 を使用することをお勧めします。出典:
binary search
バージョン 2 左閉じ、右開き [左、右)
[left, right) 左側の値は取得できますが、右側の値は取得できません。nums[m] は target と等しい可能性があるため、nums[m]>target の場合、right=m
// 版本二 左闭右开即[left, right)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size(); //左闭右开,
while (left<right)
{
int middle=left+((right-left)>>1); //防止溢出,(right-left)>>1相当于(right - left)/2
if(nums[middle]>target){
right=middle;//target在左区间,[left,middle)
}else if(nums[middle]<target){
left=middle+1;//target在右区间,[middle+1,right)
}else{
//nums[middle]==target
return middle;
}
}
// 未找到目标值
return -1;
}
};
次のコードの(right - left) >> 1 は(right - left)/2 と同等です。
int middle = left + ((right - left) >> 1);
理由:まだ left+right が基本型の最大値を超えてしまうのが不安
出典:left + ((right -left) >> 1
27 要素を削除する
配列の要素はメモリ アドレス内で連続しており、配列内の要素を個別に削除することはできず、上書きのみが可能です。
トピックリンク: 27 要素の削除
質問:
配列 nums と値 val が与えられた場合、値が val に等しいすべての要素をその場で削除し、削除された配列の新しい長さを返す必要があります。
余分な配列スペースを使用しないでください。O(1) の追加スペースのみを使用し、入力配列をインプレースで変更する必要があります。
要素の順序は変更できます。新しい長さを超える配列内の要素を考慮する必要はありません。
ブルートフォースソリューション
暴力的な解決策は 2 層の for ループであり、1 つの for ループは配列要素を走査し、2 番目の for ループは配列を更新します。
時間計算量: O(n^2)
空間計算量: O(1)
//暴力解法 两层for 循环
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size=nums.size();
for(int i=0;i<size;i++){
if(nums[i]==val){
for(int j=i+1;j<size;j++){
nums[j-1]=nums[j]; //后面的数往前移
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
ダブルポインタ方式
ダブルポインタ方式 (高速ポインタ方式と低速ポインタ方式):高速ポインタと低速ポインタを介して、1 つの for ループの下で 2 つの for ループの作業を完了します。
高速ポインタと低速ポインタを定義する
- 高速ポインタ: 新しい配列に必要な要素を検索します。新しい配列は、ターゲット要素を含まない配列 (ターゲット値を削除した後の配列) です。
- スローポインター: 新しい配列の添字が更新される場所を指します。
高速ポインタは新しい配列を作成するために使用されます。高速ポインタがターゲット値 (削除する数値) と等しくない場合、それが作成した新しい配列に必要な要素です。配列を更新する必要があります。高速ポインタで取得した値を新しい配列に対応する添字の位置に代入します。高速ポインタに低速ポインタの位置を与え、低速ポインタは新しい配列の位置を更新します。高速ポインタが低速ポインタに値を割り当てた後、更新を続けるには低速ポインタも 1 つ前の位置に戻る必要があります。高速ポインタが削除対象の要素の値に触れると、次の for ループの内容は実行されず、高速ポインタは後方に移動し続け、低速ポインタは元の位置に留まります (このステップは要素を削除します)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
//双向指针 快慢指针
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex=0;
for(int fastIndex=0;fastIndex<nums.size();fastIndex++){
if(val!=nums[fastIndex]){
nums[slowIndex++]=nums[fastIndex];
}
}
return slowIndex; //慢指针对应的下标就是新数组中的大小
}
};