• 数据结构和算法-双指针法


    参考:

    https://zhuanlan.zhihu.com/p/71643340

    https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

    双指针问题

    什么是双指针(对撞指针、快慢指针)

    双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。

    换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。

    LeetCode题库中,关于双指针的问题还是挺多的。双指针

     

    截图来之LeetCode中文官网

     

    用法

    对撞指针

    对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。

    对撞数组适用于有序数组,也就是说当你遇到题目给定有序数组时,应该第一时间想到用对撞指针解题。

    伪代码大致如下:

    function fn (list) {
      var left = 0;
      var right = list.length - 1;
    
      //遍历数组
      while (left <= right) {
        left++;
        // 一些条件判断 和处理
        ... ...
        right--;
      }
    }
    

    举个LeetCode上的例子:

    以LeetCode 881救生艇问题为例

    由于本题只要求计算出最小船数,所以原数组是否被改变,和元素索引位置都不考虑在内,所以可以先对于给定数组进行排序,再从数组两侧向中间遍历。所以解题思路如下:

    1. 对给定数组进行升序排序
    2. 初始化左右指针
    3. 每次都用一个”最重的“和一个”最轻的“进行配对,如果二人重量小于Limit,则此时的”最轻的“上船,即(left++)。不管”最轻的“是否上船,”最重的“都要上船,即(right--)并且所需船数量加一,即(num++

    代码如下:

    var numRescueBoats = function(people, limit) {
      people.sort((a, b) => (a - b));
      var num = 0
      let left = 0
      let right = people.length - 1
      while (left <= right) {
        if ((people[left] + people[right]) <= limit) {
          left++
        }
        right--
        num++
      }
      return num
    };


    题解:https://leetcode-cn.com/problems/boats-to-save-people/solution/jiu-sheng-ting-by-leetcode/

    方法:贪心(双指针)
    思路

    如果最重的人可以与最轻的人共用一艘船,那么就这样安排。否则,最重的人无法与任何人配对,那么他们将自己独自乘一艘船。

    这么做的原因是,如果最轻的人可以与任何人配对,那么他们也可以与最重的人配对。

    算法

    令 people[i] 指向当前最轻的人,而 people[j] 指向最重的那位。

    然后,如上所述,如果最重的人可以与最轻的人共用一条船(即 people[j] + people[i] <= limit),那么就这样做;否则,最重的人自己独自坐在船上。

    class Solution {
        public int numRescueBoats(int[] people, int limit) {
            Arrays.sort(people);
            int i = 0, j = people.length - 1;
            int ans = 0;
    
            while (i <= j) {
                ans++;
                if (people[i] + people[j] <= limit)
                    i++;
                j--;
            }
    
            return ans;
        }
    }
    
    作者:LeetCode
    链接:https://leetcode-cn.com/problems/boats-to-save-people/solution/jiu-sheng-ting-by-leetcode/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    复杂度分析

    • 时间复杂度:O(NlogN),其中 N 是 people 的长度。

    • 空间复杂度:O(N)






    快慢指针

    快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如fast每次增长两个,slow每次增长一个。

    LeetCode 141.环形链表为例,,判断给定链表中是否存在环,可以定义快慢两个指针,快指针每次增长一个,而慢指针每次增长两个,最后两个指针指向节点的值相等,则说明有环。就好像一个环形跑道上有一快一慢两个运动员赛跑,如果时间足够长,跑地快的运动员一定会赶上慢的运动员。

    解题代码如下:

    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    
    /**
     * @param {ListNode} head
     * @return {boolean}
     */
    var hasCycle = function(head) {
      if (head === null || head.next === null) {
        return false
      }
    
      let slow = head
      let fast = head.next
    
      while (slow !== fast) {
        if (fast === null || fast.next === null) {
          return false
        }
        slow = slow.next
        fast = fast.next.next
      }
      return true
    };
    

    再比如LeetCode 26 删除排序数组中的重复项,这里还是定义快慢两个指针。快指针每次增长一个,慢指针只有当快慢指针上的值不同时,才增长一个(由于是有序数组,快慢指针值不等说明找到了新值)。

    真实代码:

    var removeDuplicates = function (nums) {
      if (nums.length === 0) {
        return 0;
      }
      let slow = 0;
      for (let fast = 0; fast < nums.length; fast++) {
        if (nums[fast] !== nums[slow]) {
          slow++;
          nums[slow] = nums[fast];
        }
      }
      return slow + 1;
    };


    题解:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-xiang-by-/
    public int removeDuplicates(int[] nums) {
        if (nums.length == 0) return 0;
        int i = 0;
        for (int j = 1; j < nums.length; j++) {
            if (nums[j] != nums[i]) {
                i++;
                nums[i] = nums[j];
            }
        }
        return i + 1;
    }

    复杂度分析

    • 时间复杂度:O(n),假设数组的长度是 n,那么 i 和 j 分别最多遍历 n 步。

    • 空间复杂度:O(1)。



    总结

    当遇到有序数组时,应该优先想到双指针来解决问题,因两个指针的同时遍历会减少空间复杂度和时间复杂度。

    leetcode典型题目

    160. 相交链表

    注意:

    如果两个链表没有交点,返回 null.
    在返回结果后,两个链表仍须保持原有的结构。
    可假定整个链表结构中没有循环。
    程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    题解:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/

    空间复杂度 O(1) 时间复杂度为 O(n)

    这里使用图解的方式,解释比较巧妙的一种实现。

    根据题目意思
    如果两个链表相交,那么相交点之后的长度是相同的

    我们需要做的事情是,让两个链表从同距离末尾同等距离的位置开始遍历。这个位置只能是较短链表的头结点位置。
    为此,我们必须消除两个链表的长度差

    指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历
    如果 pA 到了末尾,则 pA = headB 继续遍历
    如果 pB 到了末尾,则 pB = headA 继续遍历
    比较长的链表指针指向较短链表head时,长度差就消除了
    如此,只需要将最短链表遍历两次即可找到位置
    听着可能有点绕,看图最直观,链表的题目最适合看图了

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }
    
    作者:reals
    链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    19. 删除链表的倒数第N个节点

    给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

    示例:

    给定一个链表: 1->2->3->4->5, 和 n = 2.

    当删除了倒数第二个节点后,链表变为 1->2->3->5.
    说明:

    给定的 n 保证是有效的。

    进阶:

    你能尝试使用一趟扫描实现吗?

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    题解:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/hua-jie-suan-fa-19-shan-chu-lian-biao-de-dao-shu-d/

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode removeNthFromEnd(ListNode head, int n) {    
            ListNode pre = new ListNode(0);
            pre.next = head;
            ListNode start = pre, end = pre;
            while(n != 0) {
                start = start.next;
                n--;
            }
            while(start.next != null) {
                start = start.next;
                end = end.next;
            }
            end.next = end.next.next;
            return pre.next;
        }
    }
    
    作者:guanpengchn
    链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/hua-jie-suan-fa-19-shan-chu-lian-biao-de-dao-shu-d/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    141. 环形链表

    给定一个链表,判断链表中是否有环。

    如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

    如果链表中存在环,则返回 true 。 否则,返回 false 。

    进阶:

    你能用 O(1)(即,常量)内存解决此问题吗?

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/linked-list-cycle
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    题解:https://leetcode-cn.com/problems/linked-list-cycle/solution/3chong-jie-jue-fang-shi-liang-chong-ji-bai-liao-10/

    1,快慢指针解决
    判断链表是否有环应该是老生常谈的一个话题了,最简单的一种方式就是快慢指针,慢指针针每次走一步,快指针每次走两步,如果相遇就说明有环,如果有一个为空说明没有环。代码比较简单

    public boolean hasCycle(ListNode head) {
        if (head == null)
            return false;
        //快慢两个指针
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            //慢指针每次走一步
            slow = slow.next;
            //快指针每次走两步
            fast = fast.next.next;
            //如果相遇,说明有环,直接返回true
            if (slow == fast)
                return true;
        }
        //否则就是没环
        return false;
    }
    
    作者:sdwwld
    链接:https://leetcode-cn.com/problems/linked-list-cycle/solution/3chong-jie-jue-fang-shi-liang-chong-ji-bai-liao-10/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    到这里问题好像并没有结束,为什么快慢指针就一定能判断是否有环。我们可以这样来思考一下,假如有环,那么快慢指针最终都会走到环上,假如环的长度是m,快慢指针最近的间距是n,如下图中所示

    快指针每次走两步,慢指针每次走一步,所以每走一次快慢指针的间距就要缩小一步,在图一中当走n次的时候就会相遇,在图二中当走m-n次的时候就会相遇。

    2,存放到集合中

    这题还可以把节点存放到集合set中,每次存放的时候判断当前节点是否存在,如果存在,说明有环,直接返回true,比较容易理解

    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        while (head != null) {
            //如果重复出现说明有环
            if (set.contains(head))
                return true;
            //否则就把当前节点加入到集合中
            set.add(head);
            head = head.next;
        }
        return false;
    }

    234. 回文链表

    请判断一个链表是否为回文链表。

    示例 1:

    输入: 1->2
    输出: false
    示例 2:

    输入: 1->2->2->1
    输出: true
    进阶:
    你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/palindrome-linked-list
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    题解:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/

    代码

    class Solution {
        public boolean isPalindrome(ListNode head) {
            List<Integer> vals = new ArrayList<Integer>();
    
            // 将链表的值复制到数组中
            ListNode currentNode = head;
            while (currentNode != null) {
                vals.add(currentNode.val);
                currentNode = currentNode.next;
            }
    
            // 使用双指针判断是否回文
            int front = 0;
            int back = vals.size() - 1;
            while (front < back) {
                if (!vals.get(front).equals(vals.get(back))) {
                    return false;
                }
                front++;
                back--;
            }
            return true;
        }
    }
    
    作者:LeetCode-Solution
    链接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    复杂度分析

    时间复杂度:O(n),其中 n 指的是链表的元素个数。
    第一步: 遍历链表并将值复制到数组中,O(n)。
    第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即O(n)。
    总的时间复杂度:O(2n) = O(n)。
    空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。

    作者:LeetCode-Solution
    链接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    方法三:快慢指针
    思路

    避免使用 O(n) 额外空间的方法就是改变输入。

    我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。

    该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。

    算法

    整个流程可以分为以下五个步骤:

    找到前半部分链表的尾节点。
    反转后半部分链表。
    判断是否回文。
    恢复链表。
    返回结果。
    执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。

    我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。

    若链表有奇数个节点,则中间的节点应该看作是前半部分。

    步骤二可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。

    步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。

    步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。

    代码

    class Solution {
        public boolean isPalindrome(ListNode head) {
            if (head == null) {
                return true;
            }
    
            // 找到前半部分链表的尾节点并反转后半部分链表
            ListNode firstHalfEnd = endOfFirstHalf(head);
            ListNode secondHalfStart = reverseList(firstHalfEnd.next);
    
            // 判断是否回文
            ListNode p1 = head;
            ListNode p2 = secondHalfStart;
            boolean result = true;
            while (result && p2 != null) {
                if (p1.val != p2.val) {
                    result = false;
                }
                p1 = p1.next;
                p2 = p2.next;
            }        
    
            // 还原链表并返回结果
            firstHalfEnd.next = reverseList(secondHalfStart);
            return result;
        }
    
        private ListNode reverseList(ListNode head) {
            ListNode prev = null;
            ListNode curr = head;
            while (curr != null) {
                ListNode nextTemp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = nextTemp;
            }
            return prev;
        }
    
        private ListNode endOfFirstHalf(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
            while (fast.next != null && fast.next.next != null) {
                fast = fast.next.next;
                slow = slow.next;
            }
            return slow;
        }
    }

    复杂度分析

    时间复杂度:O(n),其中 n 指的是链表的大小。

    空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。

    另附上链表反转的代码

    public class NodeReverse {
    
        static class Node {
            int val;
            Node next;
    
            public Node(int val) {
                this.val = val;
            }
    
            public Node() {
    
            }
        }
    
        public static void main(String[] args) {
            Node head = new Node(1);
            Node node1 = new Node(2);
            Node node2 = new Node(3);
            head.next = node1;
            node1.next = node2;
            Node node = head;
            head.val = 1111;
    
            while (head != null) {
                System.out.println(head.val);
                head = head.next;
            }
            Node result = reverseNode(node);
    
            while (result != null) {
                System.out.println(result.val);
                result = result.next;
            }
    
        }
    
        private static Node reverseNode(Node head) {
            Node pre = null;
            Node cur = head;
            while (cur != null) {
                Node tmpNode = cur.next;
                cur.next = pre;
                pre = cur;
                cur = tmpNode;
            }
            return pre;
        }
    
    
    }

    执行结果:

    1111
    2
    3
    3
    2
    1111

    15. 三数之和

    给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

    注意:答案中不可以包含重复的三元组。

    示例:

    给定数组 nums = [-1, 0, 1, 2, -1, -4],

    满足要求的三元组集合为:
    [
    [-1, 0, 1],
    [-1, -1, 2]
    ]

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/3sum
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    最简单的办法是,每个人都去依次拉上另一个人一起去找第三个人,这个时间复杂度是 O(n3)

        var threeSum = function(nums) {
          let res = []
          for (let i = 0; i < nums.length - 2; i++) { // 每个人
            for (let j = i + 1; j < nums.length - 1; j++) { // 依次拉上其他每个人
              for (let k = j + 1; k < nums.length; k++) { // 去问剩下的每个人
                if (nums[i] + nums[j] + nums[k] === 0) { // 我们是不是可以一起组队
                  res.push([nums[i], nums[j], nums[k]])
                }
              }
            }
          }
          return res
        }
    
    作者:wonderful611
    链接:https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    题解:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/

    class Solution {
        public static List<List<Integer>> threeSum(int[] nums) {
            List<List<Integer>> ans = new ArrayList();
            int len = nums.length;
            if(nums == null || len < 3) return ans;
            Arrays.sort(nums); // 排序
            for (int i = 0; i < len ; i++) {
                if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
                if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
                int L = i+1;
                int R = len-1;
                while(L < R){
                    int sum = nums[i] + nums[L] + nums[R];
                    if(sum == 0){
                        ans.add(Arrays.asList(nums[i],nums[L],nums[R]));
                        while (L<R && nums[L] == nums[L+1]) L++; // 去重
                        while (L<R && nums[R] == nums[R-1]) R--; // 去重
                        L++;
                        R--;
                    }
                    else if (sum < 0) L++;
                    else if (sum > 0) R--;
                }
            }        
            return ans;
        }
    }
    
    作者:guanpengchn
    链接:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    与DSP通信时,RD&WR信号
    4.2.1 Vector bit-select and part-select addressing
    数据校验
    数据结构 (树,图)
    ASOP编译说明
    os
    20180203-增与查
    yum安装MariaDB
    20180202之engine,URL,base,session
    列表
  • 原文地址:https://www.cnblogs.com/xuwc/p/13954142.html
Copyright © 2020-2023  润新知