前言

陆陆续续刷算法题已有将近100道,近日回顾总结,发现许多题目已经不会做了。遂痛定思痛,改正懒惰癌,恢复之前记笔记的习惯。之后会陆续记录刷算法的个人进度(大概一天一题的样子)和一些开源框架源码的解读。

tips

文中的题都为leetcode原题,点击标题即可跳转原链接。这里只是个人小记,欢迎大家交流,有错误和更优解还望指出

数组双指针

1. 三数之和~~~~

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

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

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
            List<List<Integer>> result = new ArrayList();        
            Arrays.sort(nums);

            for(int k=nums.length-1;k>=2;k--){
                if(nums[k]<0) break;
                int i=0,j=k-1;
                while(i<j){
                    int target = -nums[k];
                    if(nums[i]+nums[j]==target){
                        result.add(Arrays.asList(nums[i],nums[j],nums[k]));
                        while(i<j&&nums[i]==nums[i+1]) i++;
                        while(j>i&nums[j]==nums[j-1]) j--;
                        i++;j--;
                    }else if(nums[i]+nums[j]<target){
                        i++;
                    }else{
                        j--;
                    }
                }
                while(k>=2&nums[k]==nums[k-1]) k--;
            }

            return result;
    }
}

总结:三个数等于0,可以看做等价于两个数=0,假定一个数为target,另两个数的和为-target; 调整数组顺序,O(nlogn),丛数组指定一个最大的数从右向左平移,下标为k。运用双指针在在0~k-1的数组上从两侧向中间移动,找到所有等于-target,要注意边界判断和相等数略过。


2. 最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

例如,给定数组 nums = [-1,2,1,-4], 和 target = 1.

与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2).

class Solution {
    public int threeSumClosest(int[] nums, int target) {
        
        if(nums==null || nums.length<3) return -1;
        
        Arrays.sort(nums);

        int minGap =Integer.MAX_VALUE;
        int closetsum=0;

        for(int i=nums.length-1;i>=2;i--){
            int j=0,k=i-1;
            while(j<k){
                int sum = nums[i]+nums[j]+nums[k]; 
                
                if(sum==target){
                    return target;
                } else if (sum<target){
                    while(j<k&&nums[j]==nums[j+1])j++;
                    j++;
                }else{
                    while(j<k&&nums[k]==nums[k-1])k--;
                    k--;
                }
                
                int gap =  Math.abs(sum-target);
                if(minGap>gap){
                    minGap = gap;
                    closetsum = sum;
                }
            }
        }
        return closetsum;
    }
}

总结:和上一题思路一致,只是多用了两个变量来记录最小差距和最小和。


3. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:

答案中不可以包含重复的四元组。

示例:

给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。

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

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> resultList = new ArrayList();
        if(nums==null||nums.length<4) return resultList;
        Arrays.sort(nums);

        for(int k=nums.length-1;k>=3;k--){
            if(4*nums[k]<target) break;
            for(int j =k-1;j>=2;j--){
                if(3*nums[j]+nums[k]<target) break;
                int newTarget = target - nums[k]-nums[j];
                int x=0,y=j-1;

                while(x<y){
                    if(newTarget==nums[x]+nums[y]){
                        resultList.add(Arrays.asList(nums[x],nums[y],nums[j],nums[k]));
                        while(x<y && nums[x]==nums[x+1]) x++;
                        while(x<y && nums[y]==nums[y-1]) y--;
                        x++;y--;
                    }else if (newTarget<nums[x]+nums[y]){
                        y--;
                    }else{
                        x++;
                    }
                }
                while(j>=2 && nums[j]==nums[j-1]) j--;
            }
            while(k>=3 && nums[k]==nums[k-1]) k--;
        }

        return resultList;
    }
}

总结:给一个数组,求x个数之和等于一个target值或近似于一个值,求解思路都是一致的。整体时间复杂度O(n ^ x-1)。~~~~


4. 快乐数

编写一个算法来判断一个数是不是“快乐数”。

一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。

class Solution {
    public boolean isHappy(int n) {

        int fast=n,slow=n;
        while(true){
            fast = transform(transform(fast));
            if(fast==1) return true;
            slow = transform(slow);
            if(fast == slow) return false;
        }
    }
    
    public boolean isHappy2(int n) {
         Set<Integer> set = new HashSet<Integer>();
         while(true){
             n = transform(n);
             if(n==1) return true;
             if(set.contains(n)) return false;
             set.add(n);
         }
    }

    public int transform(int n){
        int result = 0;
        while(n>0){
            int temp = n %10;
            result += temp * temp;
            n=n/10;
        }
        return result;
    }

}

总结:首先需要一个辅助函数transform,求出一个数每个位置上的平方和。两种解决方法:1.初始化一个容器set,记录每次的平方和,结果要么出现1是快乐数,要么在set中已经出现,则接下来是死循环不是循环数。2.可以参考判断循环链表的思路,慢指针一次走一步,快指针一次走两步(快指针相对速度为1),如快指针为1则为快乐数,如快指针和慢指针重合则链表中有环,则不为快乐数。


5. 快乐数

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:

输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
说明:

输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
我们可以不考虑输出结果的顺序。

class Solution {
    //time:O(m+n) space:O(m+k)
    public int[] intersect(int[] nums1, int[] nums2) {
        if(nums1 == null || nums1.length ==0 || nums2 == null || nums2.length == 0 ) return new int[0];
        
        //构造Map容器,用于记录第一个数组中每个数出现的次数
        Map<Integer,Integer> statisticsMap = new HashMap();
        for(Integer num : nums1){
            if(statisticsMap.containsKey(num)){
                statisticsMap.put(num,statisticsMap.get(num)+1);
            }else{
                statisticsMap.put(num,1);
            }
        }
        
        //构造list容器,用来存放结果
        //遍历数组2,和容器map比较,若重复则保存到list中
        List<Integer> list = new ArrayList();
        for(Integer num : nums2){
            if(!statisticsMap.containsKey(num)) continue;
            list.add(num);

            if(statisticsMap.get(num)==1){
                statisticsMap.remove(num);
            }else{
                statisticsMap.put(num,statisticsMap.get(num)-1);
            }
        }

        int[] result = new int[list.size()];
        for(int i=0;i<list.size();i++){
            result[i] = list.get(i) ;
        }
        return result;
    }
    
    //time:O(m*log(m)+n*log(n)) space:O(k)
    public int[] intersect2(int[] nums1, int[] nums2) {
        if(nums1 == null || nums1.length ==0 || nums2 == null || nums2.length == 0 ) return new int[0];

        Arrays.sort(nums1);
        Arrays.sort(nums2);
        
        List<Integer> list = new ArrayList();

        int x = 0,y=0;
        while(x<nums1.length&&y<nums2.length){
            if(nums1[x]==nums2[y]){
                list.add(nums1[x]);
                x++;y++;
            }else if(nums1[x]>nums2[y]){
                y++;
            }else{
                x++;
            }

        }

        int[] result = new int[list.size()];
        for(int i=0;i<list.size();i++){
            result[i] = list.get(i);
        }

        return result;
    }
}

总结:两个数组的交集,2种解法。1:构造容器保存第一个数组每个元素出现的次数,与第二个数组进行比较,重复添加到结果容器list中;2:先对两个元素进行排序,然后运用双指针比较,若相等咋保存,且双指针+1,否则谁小谁+1,终止条件为任何一个数组达到角标边界。


6. 平方数之和

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。

示例1:

输入: 5
输出: True
解释: 1 1 + 2 2 = 5
 

示例2:

输入: 3
输出: False

class Solution {
    public boolean judgeSquareSum(int c) {
        
         int end = (int) Math.sqrt(c);

         for(int i=0;i<=end;i++){
             int n = c- i * i;
             if(isSquare(n)) return true;
         }        
          
        return false;
    }

    public boolean isSquare(int c){
        int temp = (int) Math.sqrt(c);
        return temp *temp == c;
    }
}

总结:先对数c进行求根得到end,若存在符合条件的两个数,一定在[0,end]区间内。从0开始遍历,用c减去遍历指针的平方得到结果值,若是一个平方数之和则c是平方数之和。

链表

1. 奇偶链表

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

示例 1:

输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:

输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:

应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode oddEvenList(ListNode head) {

        if(head ==null || head.next ==null) return head;
        
        ListNode odd = head;
        ListNode p2 = head.next;
        ListNode head2 = p2;
        ListNode subquentNode = p2.next;
        for(int i=1;subquentNode!=null;i++){
            if(i%2==1){
                p1.next = subquentNode;
                p1=p1.next;
            }else{
                p2.next = subquentNode;
                p2=p2.next;
            }
            subquentNode = subquentNode.next;
        }
        p2.next =null;
        p1.next = head2;
        return head;
    }
}

总结:链表的题大多都是要求空间复杂度为O(1),通过控制多个指针遍历达到最终结果。此题思路:记录奇数头结点、偶数头结点,从第三个节点开始遍历,若当前是奇数次循环则加在奇数链表尾部,否是偶数链表尾部。当节点为空,将奇数链表的尾节点的next指针指向偶数链表的头结点即可.


2. 对链表进行插入排序

对链表进行插入排序。
插入排序算法:

插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。
 
示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode insertionSortList(ListNode head) {
        if(head == null || head.next==null) return head;

        ListNode dummy = new ListNode(0),p=dummy;
        ListNode cur = head,next;

        while(cur!=null){
            next = cur.next;
            //针对每一个无序区元素,如果上一次有序区保留的p节点值大于无序区当前节点值,就无须重置p节点,可以省略掉一些不必要的比较。但不会影响具体时间间复杂度
            if(p.next!=null&&cur.val<p.next.val) p=dummy;
            while(p.next!=null&&cur.val>p.next.val)
                p=p.next;
            cur.next=p.next;
            p.next=cur;
            cur=next;
        }


        return dummy.next;
    }
}

总结:首先看局部,将一个节点插入,需要前置节点和后置节点,才能保证不破坏链表。接着继续看插入排序,将链表分为有序区和无序区。有序区头结点为dummy(新创建的节点,因为链表插入需要持有前置节点),后续遍历比较使用指针p。无序区头结点为cur(从原链表头结点开始).只要cur节点不为空和有序区p.next不为空且p.next<cur,我们就将有序区p指针后移。否则我们将cur节点插入p和p.next(可能为null)之间,然后后移无序区cur指针,直到无序区没有元素,返回dummy.next;


Alpaca
142 声望33 粉丝