目次
2. リンクリストの最後から 2 番目の N ノードを削除します。
1. リンクリストの中間ノード
876. リンクされたリストの真ん中 - LeetCode https://leetcode.cn/problems/middle-of-the-linked-list/
1. 配列:
アイデアとアルゴリズム
リンク リストの欠点は、添字を介して対応する要素にアクセスできないことです。したがって、リンクされたリストを走査し、走査された要素を配列 A に順番に入れることを検討できます。N 個の素数までトラバースすると、リンクされたリストと配列の長さも N になり、対応する中間ノードは A[N/2] になります。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
vector<ListNode*> A = {head};
while (A.back()->next != NULL)
A.push_back(A.back()->next);
return A[A.size() / 2];
}
};
複雑さの分析
-
時間計算量: O(N)、NN は指定されたリンク リスト内のノードの数です。
-
スペースの複雑さ: O(N)、つまり
A
配列によって使用されるスペース。
2. シングルポインタ方式
方法 1 でスペースの最適化を実行し、配列 A を省略できます。
リンクされたリストを 2 回たどることができます。最初のトラバーサルでは、リンク リスト内の要素の数 N をカウントします。2 回目のトラバーサルでは
、 N/2 番目の要素 (リンク リストの最初のノードは 0 番目の要素) までトラバースすると、その要素が返されます。できる。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
int n = 0;
ListNode* cur = head;
while (cur != nullptr) {
++n;
cur = cur->next;
}
int k = 0;
cur = head;
while (k < n / 2) {
++k;
cur = cur->next;
}
return cur;
}
};
複雑さの分析
-
時間計算量: O(N)、N は指定されたリンク リスト内のノードの数です。
-
スペースの複雑さ: O(1)、変数とポインターを格納するために必要なのは定数スペースのみです。
3. 高速ポインタ方式と低速ポインタ方式: (奇妙で狡猾)
アイデアとアルゴリズム
2 つのポインターを低速と高速で使用して、リンク リストを一緒に走査して、方法 2 の最適化を続けることができます。遅いとは一度に 1 歩ずつ進むことを意味し、速いとは一度に 2 歩ずつ進むことを意味します。次に、fast がリンク リストの最後に達すると、slow が中央になければなりません。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
複雑さの分析
時間計算量: O(N)、N は指定されたリンク リスト内のノードの数です。
スペースの複雑さ: O(1)。低速と高速の 2 つのポインターを格納するには一定のスペースのみが必要です。
2. リンクリストの最後から 2 番目の N ノードを削除します。
序文
リンク リストを操作する場合、一般的な手法は、 \textit{next}next ポインタがリンク リストのヘッド ノードを指すダミー ノードを追加することです。このように、ヘッド ノードに対して特別な判断を行う必要はありません。
たとえば、この質問では、ノード y を削除したい場合、ノード y の先行ノード x を知り、x のポインタを y の後続ノードに指す必要があります。ただし、ヘッド ノードには先行ノードがないため、ヘッド ノードを削除する場合は特別な判断が必要です。ただし、ダム ノードを追加すると、ヘッド ノードの先行ノードはダム ノードそのものになるため、この時点では一般的な状況のみを考慮する必要があります。
特に、一部の言語では、メモリを独自に管理する必要があります。したがって、実際の面接では面接官と積極的にコミュニケーションをとり、「削除したノードに対応する領域を解放する必要があるかどうか」について合意を得る必要があります。次のコードは、デフォルトではスペースを解放しません。
1. リンクされたリストの長さを計算します。
アイデアとアルゴリズム
簡単に考えると、まずヘッド ノードから開始してリンク リストを走査し、リンク リストの長さ L を取得します。次に、ヘッド ノードから開始してリンク リストをたどって、L-n+1 番目のノードまでたどると、それが削除する必要があるノードになります。
質問の n と矛盾しないように、ノードの番号は 1 から始まり、先頭ノードは番号 1 のノードになります。
削除操作を容易にするために、ダム ノードから開始して L-n+1 ノードをトラバースできます。L−n+1 番目のノードがトラバースされると、その次のノードが削除する必要があるノードになるため、削除操作を完了するにはポインタを 1 回変更するだけで済みます。
class Solution {
public:
int getLength(ListNode* head) {
int length = 0;
while (head) {
++length;
head = head->next;
}
return length;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
int length = getLength(head);
ListNode* cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur->next;
}
cur->next = cur->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
複雑さの分析
-
時間計算量: O(L)、LL はリンク リストの長さです。
-
空間複雑度: O(1)。
2. 積み重ねる
アイデアとアルゴリズム
リンクされたリストを走査しながら、すべてのノードを順番にスタックにプッシュすることもできます。スタックの「先入れ後出し」の原則に従って、スタックから取り出した n 番目のノードが削除する必要があるノードであり、現在スタックの一番上にあるノードが削除の先行ノードです。削除するノード。これにより、削除操作が非常に便利になります。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
stack<ListNode*> stk;
ListNode* cur = dummy;
while (cur) {
stk.push(cur);
cur = cur->next;
}
for (int i = 0; i < n; ++i) {
stk.pop();
}
ListNode* prev = stk.top();
prev->next = prev->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
複雑さの分析
-
時間計算量: O(L)、LL はリンク リストの長さです。
-
空間計算量: O(L)、LL はリンク リストの長さです。主にスタックのオーバーヘッドが原因です。
3. ダブルポインタ(高速ポインタと低速ポインタ)
アイデアとアルゴリズム
リンク リストの長さを前処理したり、定数スペースを使用したりせずに、この問題を解決することもできます。
下から n 番目のノードを見つける必要があるため、最初と 2 番目の 2 つのポインターを使用してリンク リストを同時に走査できます。最初のポインターは 2 番目のノードよりも n ノード前にあります。最初にリンク リストの最後まで移動すると、2 番目は下からちょうど n 番目のノードになります。
具体的には、最初は 1 番目と 2 番目の両方がヘッド ノードを指します。まず first を使用してリンク リストを走査します。走査回数は n です。このとき、1 番目と 2 番目の間には n-1 個のノードが存在します。つまり、1 番目のノードは 2 番目のノードよりも n 個前にあります。
その後、first と Second を同時に使用して、リンクされたリストを走査します。first がリンク リストの最後まで移動するとき (つまり、first が null ポインターである場合)、 Second は下から n 番目のノードを指すだけです。
方法 1 と方法 2 に従って、下から n 番目のノードではなく、下から n 番目のノードの先行ノードを取得できれば、削除操作がより便利になります。したがって、最初は 2 番目にダム ノードを指定することを検討でき、残りの操作ステップは変更されません。このようにして、最初にリンク リストの最後まで移動すると、2 番目の次のノードが削除する必要があるノードになります。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* first = head;
ListNode* second = dummy;
for (int i = 0; i < n; ++i) {
first = first->next;
}
while (first) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
複雑さの分析
-
時間計算量: O(L)、L はリンク リストの長さです。
-
空間複雑度: O(1)。