【アルゴリズムシリーズ】リンクリストに関するアルゴリズム

ここに画像の説明を挿入します

序文

リンクド リストは、私たちの日常生活で広く使用されているデータ構造であり、その高い拡張性と挿入と削除の利便性により、一部の分野で大きな役割を果たしています。ただし、リンク リストの独特な構造により、物理的な連続性ではなくメモリ内の論理的な連続性によって、リンク リストを操作する場合の一部のアルゴリズムも若干異なります。そこで、今日はリンク リストに関する一般的に使用される情報をいくつか共有します。

ここに画像の説明を挿入します

1. 2 つの数値を加算します

https://leetcode.cn/problems/add-two-numbers/?envType=list&envId=9Lulrn6r

1.1 質問要件

2 つの負でない整数を表す、空ではないリンク リストが 2 つ与えられます。それぞれの数字は逆の順序で保存され、各ノードは 1 つの数字のみを保存できます。

2 つの数値を加算し、その合計を同じ形式で表すリンク リストを返してください。

数字の 0 を除いて、どちらの数字も 0 で始まることはないと考えることができます。

例 1:
ここに画像の説明を挿入します

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

ヒント:

每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    
    

    }
}

1.2 疑問を解決するためのアイデア

この問題はアルゴリズムをシミュレートするという考え方を使っており、通常の足し算と同じように、2つの数値を1の位から足し、桁上げがあるかどうかに注意して最後の1桁に足していくだけです。この質問で指定されたリンク リストは逆順であるため、リンク リストのヘッド ノードから直接追加し、変数を使用してキャリーを記憶し、毎回同じビットを追加してからキャリーを追加して値を取得できます。そのビット 結果を追加します。

ここに画像の説明を挿入します
連結リストが早期に末尾に到達した場合、追加を停止するのではなく、null ノードを 0 として扱い、別のノードに追加します。また、両方の連結リストが末尾に到達したときに、キャリーが 0 でない場合は、 であることに注意してください。 、キャリーを記録するノードを作成する必要があります。

1.3 Javaコードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    
    
        ListNode head = new ListNode();
        ListNode cur = head;
        int t = 0; //用来表示进位
        while(l1 != null || l2 != null || t != 0) {
    
    
            int sum = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + t;
            ListNode node = new ListNode(sum % 10);
            cur.next = node;
            cur = cur.next;
            t = sum / 10;

            l1 = (l1 == null ? null : l1.next);
            l2 = (l2 == null ? null : l2.next);
        }

        return head.next;
    }
}

ここに画像の説明を挿入します
ここにヘッド ノードを作成することを選択したのはなぜですか? このノードはセンチネルビットに似ており、このセンチネルビットを使用することで、返されたリンクリストの先頭ノードが空である場合の判定を軽減することができます。リンク リストに関連する質問を実行するときは、メモリ領域をケチるべきではなく、追加のノードを作成することで、不必要なトラブルを大幅に節約できます。
ここに画像の説明を挿入します

2. リンクリスト内のノードをペアごとに交換します

https://leetcode.cn/problems/swap-nodes-in-pairs/?envType=list&envId=9Lulrn6r

2.1 質問要件

リンクされたリストを与え、隣接するノードをペアで交換し、交換されたリストのヘッド ノードを返します。ノード内の値を変更せずにこの質問に回答する必要があります (つまり、ノードのスワップのみを実行できます)。

例 1:
ここに画像の説明を挿入します

输入:head = [1,2,3,4]
输出:[2,1,4,3]

例 2:

输入:head = []
输出:[]

例 3:

输入:head = [1]
输出:[1]

ヒント:

链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode swapPairs(ListNode head) {
    
    

    }
}

2.2 疑問を解決するためのアイデア

この質問では、リンク リスト全体の逆順ではなく、隣接する 2 つのノードごとに交換されるため、交換するたびに、どの 2 つのノードが交換されるかを知る必要があり、また、これら 2 つのノードのうち最後の 1 つを覚えておく必要があります。そうしないと、交換が完了したときに後続のノードの内容がわかりません。
ここに画像の説明を挿入します

2.3 Javaコードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode swapPairs(ListNode head) {
    
    
        if(head == null || head.next == null) return head;
        ListNode newHead = new ListNode();
        ListNode prev = newHead, cur = head, next = head.next, nnext = next.next;
        //当链表的节点数为偶数的时候,循环结束的条件就是cur为null;
        //如果节点数为奇数的时候,循环结束的条件是next为null
        while(cur != null && next != null) {
    
    
            //交换相邻两个链表
            prev.next = next;
            next.next = cur;
            cur.next = nnext;

            //更新prev cur next 和nnext的信息
            prev = cur;
            cur = nnext;
            if(cur != null) next = cur.next;
            if(next != null) nnext = next.next;
        }

        return newHead.next;
    }
}

ここに画像の説明を挿入します

3. リンクされたリストを並べ替えます

https://leetcode.cn/problems/reorder-list/?envType=list&envId=9Lulrn6r

3.1 質問の要件

単結合リスト L の先頭ノード head を考えると、単結合リスト L は次のように表されます。

L0 → L1 → … → Ln - 1 → Ln

これを次のように並べ替えてください。

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

ノード内の値を変更するだけではなく、実際にノードを交換する必要があります。

例 1:

ここに画像の説明を挿入します

输入:head = [1,2,3,4]
输出:[1,4,2,3]

例 2:

ここに画像の説明を挿入します

输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]

ヒント:

链表的长度范围为 [1, 5 * 104]
1 <= node.val <= 1000
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public void reorderList(ListNode head) {
    
    

    }
}

3.2 疑問を解決するためのアイデア

質問の意味によれば、リンクリスト L0 → L1 → … → Ln - 1 → Ln を L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …、つまり最初のリストに再配置する必要があります。ノードの後に​​最後の 1 つのノードが続き、次に最後のノードの後に​​ 2 番目のノードが続き、2 番目のノードの後に​​最後から 2 番目のノードが続きます...しかし、コードでこれを解決するにはどうすればよいでしょうか? メモリ内のリンク リストの位置はランダムであるため、リンク リストへのランダム アクセスを実現することはできません。では、最初のノードと最後のノードにアクセスし、同時に 2 番目のノードの位置を記憶するにはどうすればよいでしょうか?

リンクされたリストの後半を反転し、後半の各ノードを前半の 2 つのノードの間に挿入できます。

ここに画像の説明を挿入します

3.3 Java コードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public void reorderList(ListNode head) {
    
    
        //1.找到链表的中间节点
        ListNode slow = head, fast = head, thead = head;
        while(fast != null && fast.next != null) {
    
    
            slow = slow.next;
            fast = fast.next.next;
        }

        //将前半部分的最后一个节点的next置为null,防止出现环
        ListNode tmp = slow.next;
        slow.next = null;
        slow = tmp;
        
        //2.翻转slow后面部分的链表
        fast = null;
        while(slow != null) {
    
    
            ListNode next = slow.next;
            slow.next = fast;
            fast = slow;
            slow = next;
        }
        
        //3.将后半部分的链表依次加入到前半部分的两个节点之间
        slow = fast; //将slow更改为后半部分倒置后链表的第一个节点
        ListNode cur = thead;
        while(slow != null) {
    
    
            ListNode nextCur = cur.next, nextSlow = slow.next;
            slow.next = nextCur;
            cur.next = slow;
            cur = nextCur;
            slow = nextSlow;
        }
    }
}

ここに画像の説明を挿入します

スローから始まるリンク リストを反転するのではなく、スローの後の部分を反転することを選択したのはなぜですか? リンクリストをスローから部分的に反転することも可能ですが、その場合リンクリストの前半の長さが後半よりも短くなりますので、ノードを失いたくない場合は追加の判断が必要です。
ここに画像の説明を挿入します

4. k 個の昇順リンクリストを結合します。

https://leetcode.cn/problems/vvXgSW/?envType=list&envId=9Lulrn6r

4.1 質問要件

リンク リストの配列が与えられると、各リンク リストは昇順に並べ替えられます。

すべてのリンク リストを昇順のリンク リストに結合し、結合されたリンク リストを返してください。

例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
 1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

例 2:

输入:lists = []
输出:[]

例 3:

输入:lists = [[]]
输出:[]

ヒント:

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode mergeKLists(ListNode[] lists) {
    
    

    }
}

4.2.1 質問 1 のアイデア

この質問の要件は、k 個の昇順リンク リストをマージすることであり、これは 2 つの昇順リンク リストをマージするのと似ていますが、単に 2 つの昇順リンク リストをマージする方法を使用して k 個の昇順リンク リストを解く場合、時間計算量は比較的長くなります。高い場合、時間の複雑さを軽減する方法はありますか?

この質問の時間を短縮するには 2 つの方法があります。

  1. 優先キューを使用します。小さなルート ヒープを作成し、最初に各リンク リストのヘッド ノードを優先キューに入れ、次にヒープの先頭にあるノードを取り出します (ヒープの先頭にあるノードは、現在のヒープ内のすべての要素の中で最小です) heap)、リンク リストは昇順に配置されているため、すべてのリンク リストの最小の要素が最初に取り出され、その後、取り出されたノードがどのリンク リストに属していても、リンク リストの次のノードが配置され続けます。優先キューに入れて操作を続行します。

ここに画像の説明を挿入します

4.3.1 方法 1 Java コードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode mergeKLists(ListNode[] lists) {
    
    
        PriorityQueue<ListNode> heap = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
        //将每个链表的头节点放入优先级队列中
        for(ListNode l : lists) {
    
    
            if(l != null) {
    
    
                heap.offer(l);
            }
        }

        ListNode ret = new ListNode();
        ListNode cur = ret;
        while(!heap.isEmpty()) {
    
    
            //将堆顶的节点添加到返回链表中
            ListNode tmp = heap.poll();
            ListNode node = new ListNode(tmp.val);
            cur.next = node;
            cur = cur.next;
            //插入取出链表的下一个节点
            if(tmp.next != null) heap.offer(tmp.next);
        }

        return ret.next;
    }
}

ここに画像の説明を挿入します

4.2.2 質問解決のアイデア 2

なぜなら、2 つの順序付き昇順リンク リストをマージする方法は以前に知っているからですが、2 つのリンク リストをマージするために総当り的なソリューションを使用すると、時間の計算量が非常に高くなります。 2 つの順序付きリンク リストをマージしますが、時間は複雑ではありません。あまり複雑ではないメソッドはどうでしょうか。言い換えれば、2 つの昇順リンク リストをマージすることで k 個の昇順リンク リストをマージするにはどうすればよいでしょうか? もちろん、それはあります。この種の考え方は、大きなものを小さなものに還元することであり、マージのアイデアも、大きなものを小さなものに還元することです。したがって、この問題を解決する 2 番目の方法は、マージを使用することです。それを解決する方法。

ここに画像の説明を挿入します

4.3.2 方法 2 Java コードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode mergeKLists(ListNode[] lists) {
    
    
        return merge(lists, 0 ,lists.length - 1);
    }

    private ListNode merge(ListNode[] lists, int left, int right) {
    
    
        //当left > right 的时候,说明传入的lists不和逻辑
        if(left > right) return null;
        //如果left == right,说明lists中只有一个链表,直接返回
        if(left == right) return lists[left]; 
        int mid = left + (right - left) / 2;

        //递归
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid  + 1, right);

        //合并连个有序链表
        return mergeTwoLists(l1,l2);
    }

    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        if(l1 == null) return l2;
        if(l2 == null) return l1;

        ListNode head = new ListNode();
        ListNode cur = head; 
        while(l1 != null && l2 != null) {
    
    
            if(l1.val < l2.val) {
    
    
                cur.next = l1;
                l1 = l1.next;
            }else {
    
    
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }

        if(l1 != null) cur.next = l1;
        if(l2 != null) cur.next = l2;

        return head.next;
    }
}

ここに画像の説明を挿入します

5. k 個の反転されたリンク リストのセット

https://leetcode.cn/problems/reverse-nodes-in-k-group/

5.1 質問の要件

リンク リストのヘッド ノードを指定し、グループ内の k 個のノードごとに反転し、変更されたリンク リストを返してください。

k は、値がリンクされたリストの長さ以下の正の整数です。ノードの総数が k の整数倍ではない場合は、最後に残ったノードを元の順序のままにしてください。

ノード内の値を変更するだけではなく、実際にノードを交換する必要があります。

例 1:
ここに画像の説明を挿入します

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

例 2:
ここに画像の説明を挿入します

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

ヒント:

链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode reverseKGroup(ListNode head, int k) {
    
    

    }
}

上級: O(1) 個の追加メモリ空間のみを使用して、この問題を解決するアルゴリズムを設計できますか?

5.2 疑問を解決するためのアイデア

この質問の要件は、リンク リストを k 個のグループで反転することです。実際、本質的にはリンク リストを反転しているだけです。リンク リストを n 個のグループに分割し、各グループ内の k 個のノードを反転するだけです。次に、最初に反転するリンク リストのグループの数を見つけてから、グループごとにリンク リストを反転する操作を実行できます。
ここに画像の説明を挿入します

5.3 Javaコードの実装

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode reverseKGroup(ListNode head, int k) {
    
    
        //1.统计出需要翻转多少组

        //先预处理一遍,统计出链表节点的个数
        int n = 0;
        ListNode cur = head;
        while(cur != null) {
    
    
            n++;
            cur = cur.next;
        }
        n /= k;  //n就是需要翻转的组数

        //翻转这n组链表
        ListNode newHead = new ListNode();
        ListNode prev = newHead;
        cur = head;
        for(int i = 0; i < n; i++) {
    
    
            ListNode tmp = cur;
            //每组翻转 k 个节点
            for(int j = 0; j < k; j++) {
    
    
                ListNode next = cur.next;
                cur.next = prev.next;
                prev.next = cur;
                cur = next;
            }
            //当翻转完成一组之后,将prev更新为翻转完成之后的最后一个节点
            prev = tmp;
        }

        prev.next = cur;
        return newHead.next;
    }
}

ここに画像の説明を挿入します

要約する

リンク リストに関連するアルゴリズムの質問を行うときは、スペースの無駄をケチるべきではありません。追加の仮想ノードを作成すると、不要なトラブルを大幅に節約できます。リンク リストのメモリは連続的ではなくランダムであるため、次の点に注意する必要があります。次のノードの位置を保存しないと、後のノードが失われます。

おすすめ

転載: blog.csdn.net/m0_73888323/article/details/133298096