LeetCode.双指针(二)

例题一 

一、题目

颜色分类
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]
 

提示:

n == nums.length
1 <= n <= 300
nums[i] 为 0、1 或 2
 

进阶:

你能想出一个仅使用常数空间的一趟扫描算法吗?

作者:LeetCode
链接:https://leetcode.cn/leetbook/read/all-about-array/x9wv2h/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二、代码

思路1:

        他强任他强,遇到库还得趴地上

class Solution {
    public void sortColors(int[] nums) {
        Arrays.sort(nums);
    }
}

时间复杂度:O(nlogn)//java当中Arrays.sort()采用的是归并排序

空间复杂度:O(n)

思路2:

        仔细观察,我们可以发现,数组中只有三种元素0,1,2,那么只需排序好两种元素,剩下一种自然就好了。那么先排序哪两种元素呢?在实现过程可以发现先排序0,2更加容易,因为0,2分别在首尾,剩余中间的就全是1。

        方法:

            1.设置三个指针i,j,k。i指向新数组第一个元素,j指向最后一个元素,k遍历原数组。

            2.当nums[k]==0,则赋值给新数组的首部;当nums[k] == 2,则赋值给新数组的尾部;

            3.当k遍历结束,新数组剩余的元素填充1

            4.最后把新数组的元素赋值给旧数组

void sortColors(int* nums, int numsSize){
    int i = 0;
    int j = numsSize - 1;
    int newNums[numsSize];
    for(int k  = 0; k < numsSize; k++){
        if(nums[k] == 0){
            newNums[i++] = 0;
        }
        if(nums[k] == 2){
            newNums[j--] = 2;
        }
    }
    while(i <= j){
        newNums[i] = 1;
        i++;
    }
    for(int k = 0; k < numsSize; k++){
        nums[k] = newNums[k];
    }
}

时间复杂度:O(n)

空间复杂度:O(n)

显然,思路1有些繁琐,需要把元素放到新的数组,然后再放到就数组中。那么可以不可以直接在原数组中操作呢?

思路3:

        我们把思路2的操作,直接在原数组中实现。可以发现当nums[k] == 0或2时,直接赋值给nums[i]或nums[j]时会导致数组中未排好序的元素被覆盖了。这该如何解决呢?可以交换这两个元素是否可行呢?

void sortColors(int* nums, int numsSize){
    int newArray[numsSize];
    int head = 0;
    int tail = numsSize - 1;
    int res = 0;
    for(int i = 0; i < numsSize; i++){
        if(head > tail || i > tail)break;
        if(nums[i] == 0){
            // newArray[head++] = 0;
            res = nums[head];
            nums[head] = nums[i];
            nums[i] = res;
            head++;
        }else if(nums[i] == 2){
            // newArray[tail--] = 2;
            res = nums[tail];
            nums[tail] = nums[i];
            nums[i] = res;
            tail--;
            i--;//避免遗漏,当nums[i] = 0 && nums[tail]=2时,交换后i++,会导致0元素遗漏
        }else{
            continue;
        }
    }
    while(head <= tail){
        newArray[head] = 1;
        head++;
    }
}

实践证明是可行的,但会出现一个问题:

如,当nums[j]中就是2或者当nums[i]中本就是0时,而nums[k]也是0或者2,此时交换会导致原有的排好序的元素被交换出来。此时,当我们交换后,再次检查交换后nums[k]元素是否为2或者0,防止遗漏,那在代码上,由于交换后k会自动下移,此时只需要k--回到上一个位置即可。

例题二

一、题目

数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:

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

提示:

1 <= k <= nums.length <= 105
-104 <= nums[i] <= 104
相关标签

C

作者:LeetCode
链接:https://leetcode.cn/leetbook/read/all-about-array/x90rpm/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二、代码

思路1:直接上轮子,结束(大雾)
class Solution {
    public int findKthLargest(int[] nums, int k) {
        Arrays.sort(nums);
        return nums[nums.length - k];
    }
}

时间复杂度:O(nlogn)

空间复杂度:O(1)

思路2:TOP(K)类型的题目

        一般思路,排序 + 返回第k个最大的数字,但本题要求时间复杂度为O(n),可是排序算法最快的也要O(nlogn),看来不能简单的采用"排序+返回第k个“方法。那是否是排序的变种呢?与O(n)最接近的为O(nlogn),那试试是否可以采用快速排序的变种?

        回顾快速排序算法的思想:

                1.找一个基准数k

                2.把小于基准数k的元素放到基准数的左边,大于基准数k的元素放到基准数的右边

                     采用对撞指针,i指向左边,不断的i++,j指向右边,不断的j--,当nums[i] >k时停止,

                     当nums[j] < k时停止,然后交换两个元素;

                     直至i==j时,把nums[i] 与 k交换,

                     此时,确定了k的位置。

                3.对于基准数k左右两边的数组分别重复步骤1,2

                4.直至每个部分中只有一个元素,排序结束

        我们可以发现快排每次确定一个数k的位置,假设k的索引为index,那么index+ 1不就是第index+1大的元素么?如k在nums[0]上,k为第1大的元素。假设要找第target大的元素,如果tartget  < index + 1 ,则说明第target大的元素在k的左侧,即只需要排序k左侧的元素即可;同理,target > index + 1,则只需排序k的右侧数组;若taget == index + 1,则第target大的元素就是k。

同时,可以发现,每次都只需排序一个部分,而不向原快排,每次递归排序两个部分,则此时时间复杂度接近O(n)。

int findKthLargest(int* nums, int numsSize, int k){
    return QuickSort(nums,0,numsSize - 1,k);;
}

void swap(int *nums, int i ,int j){
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

int QuickSort(int *nums,int low,int high, int k){
    int base = nums[low];
    int i = low;
    int j = high;
    if(i >= j){//如果指针相遇说明只有一个元素,不需要排序
        return nums[i];
    }
    while(i < j){
        //快速排序(从小到大),一定要先从右边开始,举例:6,1,1,2,8,9
        //如果从左边开始,则nums[i] = 8,nums[j] = 8,然后交换 6 和 8,导致基准数6的位置错误
        //右边的指针向左走,遇到小于base就停下
        while(nums[j] <= base && i < j){
            j--;
        }
        //左边的指针向右走,遇到大于base的就停下
        while(nums[i] >= base && i < j){
            i++;
        }
        //交换两个数
        swap(nums,i,j);
    }
    
    //确定基准数的位置
    swap(nums,low,i);
    
    //由于快速排序(从大到小)每趟都能确定一个数的位置,我们可以根据这个来找出第k大的数字
    //若index >= k ,则说明第k大的数字在左边继续排序左边的;若index < K,则第k大的数字在右边继续排序右边的
    if(i > k - 1){
       return QuickSort(nums,low,i - 1, k);//排序左边
    }
    if (i < k - 1){
        //把分下来的两个数组继续排序
       return QuickSort(nums,i+1,high,k);//排序右边
    }
    return nums[i];
}

时间复杂度:O(n)

空间复杂度:O(1)

例题三

一、题目

合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:

输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
 

提示:

nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109
 

进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?

作者:LeetCode
链接:https://leetcode.cn/leetbook/read/all-about-array/x9lhe7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二、代码

思路1:头插法

        1.i指向nums1,j指向nums2

        2.依次判断,nums1[i] 与 nums2[j] 的大小

        3.由于数组为非递减,所以,不会出现,后一个比前一个数小的情况

        4.选择两者中较小的元素放置到新数组中

        5.把新数组的元素放置到nums1中

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    //双指针问题
    //i指向nums1,j指向nums2
    //依次判断,nums1[i] 与 nums2[j] 的大小
    //由于数组为非递减,所以,不会出现,后一个比前一个数小的情况
    //选择两者中较小的元素放置
    int newNums[nums1Size];
    int i = 0;
    int j = 0;
    int k = 0;
    //把两个数组的元素排序放到新数组中,时间复杂度O(m+n)
    for(i = 0,j = 0,k = 0; i < m && j < n && k < nums1Size;k++){
        if(nums1[i] <= nums2[j]){
            newNums[k] = nums1[i++];
        }else{
            newNums[k] = nums2[j++];
        }
    }
    while(i < m && k < nums1Size){
        newNums[k++] = nums1[i++];
    }
    while(j < n && k < nums1Size){
        newNums[k++] = nums2[j++];
    }
    //把新数组中的元素放到nums1中,时间复杂度O(m+n)
    for(i = 0; i < nums1Size; i++){
        nums1[i] = newNums[i];
    }
}

时间复杂度:O(m + n + n)

空间复杂度:O(m + n)

思路2:尾插法

        是否可以直接在原数组中操作?使得空间复杂度为O(1),时间复杂度为O(m+n)?由于nums1数组的尾部有空间,可以直接从尾部开始插入,因此在比较的时候选择大的元素插入尾部。

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    //从后向前,找较大的元素放到nums1中
    int i = m - 1;
    int j = n -1;
    int k = nums1Size - 1;
    for(;i >= 0 && j >= 0 &&k >= 0; k--){
        if(nums1[i] >= nums2[j]){
            nums1[k] = nums1[i--];
        }else{
            nums1[k] = nums2[j--];
        }
    }
    while(i>=0 && k >= 0){
        nums1[k--] = nums1[i--];
    }

    while(j >=0 && k>=0){
        nums1[k--] = nums2[j--];
    }
}

时间复杂度:O(n)

空间复杂度:O(1)

三、总结

        相比较双指针(一),本次几个双指针题目更加灵活一点,尤其是例题二,需要对已学过的算法的改良。这需要我们对于一些基础算法有较深理解,以及在此基础上做出适当的修改,这对编码能力有较高的要求。

你可能感兴趣的:(LeetCode,leetcode,算法)