C++で連結リストをO(n log n)時間・定数空間でソートする方法
連結リストが与えられたとき、これをO(n log n)の時間計算量・定数空間計算量で並べ替えることを考えます。たとえば、リストが [4,2,1,3] である場合、出力は [1,2,3,4] となります。
この問題はマージソートの考え方を応用することで効率的に解決できます。連結リストはランダムアクセスができないためクイックソートやヒープソートは不向きですが、マージソートならリスト構造に適した形で、余分な配列領域を使わずに実装できます。
アルゴリズムの手順
まず、2つのソート済みリストを昇順に統合するための補助メソッド merge() を定義します。このメソッドは2つのリスト l1 と l2 を受け取り、値を比較しながら1つの整列済みリストにまとめます。
続いて、sortList() メソッドは以下のように動作します。
- head が NULL、または head の next が NULL の場合は head をそのまま返します(要素が0個または1個のため、すでにソート済みとみなせます)。
- slow / fast ポインタのテクニックを使ってリストの中央位置を求めます。slow := head、fast := head、prev := NULL と初期化します。
- fast が NULL でなく、かつ fast の next も NULL でない限り、以下を繰り返します。
- prev := slow
- slow := slow の next
- fast := fast の next の next
- ループ終了後、prev の next を NULL に設定して、リストを前半と後半の2つに分割します。
- l1 := sortList(head)、l2 := sortList(slow) として、それぞれの半分を再帰的にソートします。
- 最後に merge(l1, l2) の結果を返します。
slow ポインタは1ステップずつ、fast ポインタは2ステップずつ進むため、fast が末尾に到達したとき slow はちょうどリストの中央付近にあります。これにより、配列のようにインデックスアクセスできない連結リストでも、効率よく二分割できます。
実装例
理解を深めるために、以下のC++による実装を見てみましょう。
#include <bits/stdc++.h>
using namespace std;
class ListNode{
public:
int val;
ListNode *next;
ListNode(int data){
val = data;
next = NULL;
}
};
ListNode *make_list(vector<int> v){
ListNode *head = new ListNode(v[0]);
for(int i = 1; i<v.size(); i++){
ListNode *ptr = head;
while(ptr->next != NULL){
ptr = ptr->next;
}
ptr->next = new ListNode(v[i]);
}
return head;
}
void print_list(ListNode *head){
ListNode *ptr = head;
cout << "[";
while(ptr){
cout << ptr->val << ", ";
ptr = ptr->next;
}
cout << "]" << endl;
}
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head || !head->next)return head;
ListNode *slow = head, *fast = head, *prev = NULL;
while(fast && fast->next){
prev = slow;
slow = slow->next;
fast = fast->next->next;
}
prev->next = NULL;
ListNode* l1 = sortList(head);
ListNode* l2 = sortList(slow);
return mergeList(l1,l2);
}
ListNode* mergeList(ListNode* l1, ListNode* l2){
ListNode* temp = new ListNode(0);
ListNode* p =temp;
while(l1 && l2){
if(l1->val<=l2->val){
p->next = l1;
l1 = l1->next;
}else{
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
if(l1){
p->next = l1;
}
if(l2){
p->next = l2;
}
return temp->next;
}
};
main(){
vector<int> v = {4,2,1,3,5,19,18,6,7};
ListNode *h1 = make_list(v);
Solution ob;
print_list((ob.sortList(h1)));
}
入力
[4,2,1,3,5,19,18,6,7]
出力
[1, 2, 3, 4, 5, 6, 7, 18, 19]
計算量について
リストを毎回ほぼ半分に分割しながら再帰的に処理するため、時間計算量は O(n log n) になります。また、マージ処理ではノードのポインタをつなぎ変えるだけで新しいノードをほとんど確保しないため、再帰呼び出しのスタックを除けば定数空間で動作します。連結リストのソートが必要な場面では、この手法が最も標準的かつ効率的なアプローチといえます。
-
C++で「次に小さい要素」を求める方法|スタックを使った効率的なアルゴリズム
次に小さい要素とは?「次に小さい要素(Next Smaller Element)」とは、ある要素よりも後ろに位置する要素の中で、最初に現れる「より小さい値」のことです。具体例を見てみましょう。arr = [1, 2, 3, 5, 4]この配列では、5 の次に小さい要素は 4 です。一方、1・2・3 の後ろにはそれらより小さい要素が存在しないため、答えは -1 になります。アルゴリズムこの問題はスタックを活用することで効率的に解けます。手順は以下の通りです。配列をランダムな数値で初期化します。スタックを初期化し、最初の要素をプッシュします。配列の各要素を順に走査します。スタックが空の場合は、現在
-
C++でランダムポインタを持つリンクリストをディープコピーする方法
ランダムポインタを持つリンクリストとはリンクリスト(連結リスト)は代表的な線形データ構造の一つで、各ノードは「ノードが保持する値(データ)」と「次のノードのアドレスを格納するポインタ(next)」という2つの部分で構成されます。本記事では、さらに各ノードがリスト内の別のノードを指す「ランダムポインタ(random)」を持つリンクリストを扱います。このようなリストに対して、元のリストと同じデータ・同じランダムポインタ構造を持つ新しいリストを作成することを、リンクリストの「ディープコピー(Deep Copy)」と呼びます。例入力:出力:5-> 2 -> 3 -> 7 ->4