数组、链表、跳表

73 阅读8分钟

用数组实现一个栈和队列

//核心思想就是System.arraycopy方法,和当前数据的一个大小
static class Queue<T> {
    private Object[] array;
    private int size;

    public Queue(int cap) {
        array = new Object[cap];
    }

    public void add(T t) {
        array[size++] = t;
    }

    public T remove() {
        if (size == 0) {
            return null;
        }
        T t = (T) array[0];
        System.arraycopy(array, 1, array, 0, --size);
        array[size] = null;
        return t;
    }
}

static class Stack<T> {
    private Object[] array;
    private int size;

    public Stack(int cap) {
        array = new Object[cap];
    }

    public void add(T t) {
        array[size++] = t;
    }

    public T remove() {
        if (size == 0) {
            return null;
        }
        T t = (T) array[--size];
        array[size] = null;
        return t;
    }
}

盛最多水的容器

leetcode地址

//使用双指针的夹逼的方式来进行计算
//因为最多水,就是尽可能的宽和高,当需要缩短宽时,需要尽量保证高不会缩短
public int maxArea(int[] height) {
    int left = 0;
    int right = height.length - 1;
    int maxArea = 0;
    while (left < right) {
        int area = Math.min(height[left], height[right]) * (right - left);
        if (maxArea < area) {
            maxArea = area;
        }
        if (height[left] > height[right]) {
            right--;
        } else {
            left++;
        }
    }
    return maxArea;

}

移动零

leetcode地址

//正常的暴力求解应该是n^2
//因为这里采取了补齐操作,先把非0的移动,然后对零进行补齐
public void moveZeroes(int[] nums) {
    int zeroIndex = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] != 0) {
            if (zeroIndex < i) {
                nums[zeroIndex] = nums[i];
            }
            zeroIndex++;
        }
    }
    for (int i = zeroIndex; i < nums.length; i++) {
        nums[i] = 0;
    }
}

爬楼梯问题(斐波拉契数列)

leetcode地址

//该题可以使用递归f(n) = f(n-1)+f(n-2),2^n时间复杂度
//使用递归的时候可以优化,使用map缓存,可以少计算很多,n的时间复杂度
//下面这种是自底向上编程方式,n的时间复杂度,空间复杂度也很小
public int climbStairs(int n) {
    int f1 = 1;
    int f2 = 2;
    if (n == 1) {
        return f1;
    } else if (n == 2) {
        return f2;
    }
    for (int i = 3; i <= n; i++) {
        int temp = f2;
        f2 = f1 + f2;
        f1 = temp;
    }
    return f2;
}

两数之和

leetcode地址

//可以使用暴力求解,注意不能重复也就是二层循环不能从下标0开始,时间复杂度O(n^2)
//这里使用了哈希表缓存,就不需要再次遍历了,直接在hash表中取数据就可以,时间复杂度O(n)
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> cache = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        if (cache.get(nums[i]) != null) {
            return new int[]{cache.get(nums[i]), i};
        }
        cache.put(target - nums[i], i);
    }
    return new int[0];
}

三数之和

leetcode地址

//核心是先排序再利用双指针左右夹逼的方式进行处理,很多逻辑判断都是判断不能重复
//为什么没有用map缓存,是因为不能重复map缓存之后对数组下标就有了很多限制
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);
    Map<Integer, Integer> cacheMap = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        cacheMap.put(-nums[i], i);
    }
    for (int i = 0; i < nums.length; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }
        
        for (int j = i + 1, k = nums.length - 1; j < k; ) {

            if (j > i + 1 && nums[j] == nums[j - 1]) {
                j++;
                continue;
            }
            if (k < nums.length - 1 && nums[k] == nums[k + 1]) {
                k--;
                continue;
            }
            if (nums[i] + nums[j] + nums[k] == 0) {
                list.add(Arrays.asList(nums[i], nums[j], nums[k]));
                j++;
                k--;
            } else if (nums[i] + nums[j] + nums[k] > 0) {
                k--;
            } else {
                j++;
            }
        }
    }
    return list;
}

链表反转

leetcode地址

//1->2->3->4->5
//双指针操作,有点类似两个数据交换,curr.next、curr、pre相互交换
public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode next = curr.next;
        curr.next = pre;
        pre = curr;
        curr = next;
    }
    return pre;
}

//递归的方式
public ListNode reverseList(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    ListNode ret = reverseList(head.next);
    //大概就是先全部递归如虚拟机栈,然后调转方向设置next
    head.next.next = head;
    head.next = null;
    return ret;
}

两两交换链表中的节点

leetcode地址

//1->2->3->4->5
//递归的实现,和链表反转有点类似,只不过比反转会多了一个next,每次都把两个head和head.next看成一个单元
public ListNode swapPairs(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }

    ListNode next = head.next;
    head.next = swapPairs(next.next);
    next.next = head;
    return next;
}

//非递归的方式实现
//没有必要两个节点两个节点为一组循环一起,人肉递归容易走进这样的误区
//归根结底还是两个节点交换,上一个节点进行next替换就可以了

//最重要的一点就是当感觉思绪混乱的时候,需要拆分循环,这里也是拆分为两个部分
//1. 需要保存上一个节点,放在循环体外,一次循环两个节点后就需要把两个节点的head的上一个连接
//2. 循环体内就是两个节点互相交换
public ListNode swapPairs(ListNode head) {
    ListNode pre = new ListNode(0);
    pre.next = head;
    ListNode curr = pre;
    while (curr.next != null && curr.next.next != null) {
        ListNode start = curr.next;
        ListNode end = start.next;
        
        //交换
        start.next = end.next;
        end.next = start;
        curr.next = end;
        curr = start;
    }
    return pre.next;
}

环形链表

leetcode地址

//这里使用的是快慢指针,也可以用哈希表将已经遍历访问过的元素进行缓存
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head.next;
    while (fast != null && fast.next != null) {
        if (fast == slow) {
            return true;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return false;
}

环形链表 II

leetcode地址

//还是可以用hash缓存访问过的元数据,遇到重复就是第一个环的起点

//下面用的是双指针法,可以参考leetcode中的题解的公式推导过程
//假设无环形部分节点个数为a,有环形节点个数为b
//第一次相遇:f=2s;f=s+nb(快节点走了环形n圈了),得到s=nb,所以想要回到环开始的点位,就需要s再走a个节点,所以可以另外起一个指针从头开始走,和s指针相遇的点就是环形的起点
//大白话叙述就是快慢指针第一次相遇后肯定是都走到了环,至少快指针比慢指针多走了一圈
public ListNode detectCycle(ListNode head) {
    if (head == null || head.next == null) {
        return null;
    }
    ListNode slow = head;
    ListNode fast = head;
    while (true) {
        if (fast == null || fast.next == null) {
            //无环
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
        if (fast == slow) {
            break;
        }
    }
    fast = head;
    while (fast != slow) {
        slow = slow.next;
        fast = fast.next;
    }
    return fast;
}

k个一组翻转链表

leetcode地址

//1->2->3->4->5->6->7->8->9->10
//1<-2<-3<-4<-5<-6<-7<-8<-9<-10
public ListNode reverseKGroup(ListNode head, int k) {
    ListNode temp = head;
    ListNode newHeader = head;
    ListNode lastGroupTail = null;
    while (temp != null) {
        ListNode currHead = temp;
        int count = 0;
        for (; count < k && temp != null; count++) {
            temp = temp.next;
        }

        //反转group链表
        ListNode pre;
        if (count == k) {
            pre = reverse(currHead, k);
        } else {
            pre = currHead;
        }

        //每个group首尾相连
        if (lastGroupTail != null) {
            lastGroupTail.next = pre;
        } else {
            newHeader = pre;
        }
        lastGroupTail = currHead;
    }
    return newHeader;
}

//翻转
private ListNode reverse(ListNode head, int k) {
    ListNode curr = head, pre = null;
    while (curr != null && k-- > 0) {
        ListNode next = curr.next;
        curr.next = pre;
        pre = curr;
        curr = next;
    }
    return pre;
}

删除有序数组中的重复项

lettcode地址

//双指针,一般只是替换值,就用等于赋值,不要用System#arraycopy
public static int removeDuplicates(int[] nums) {
    int n = nums.length;
    if (n == 0) {
        return 0;
    }
    int fast = 1, slow = 1;
    while (fast < n) {
        if (nums[fast] != nums[fast - 1]) {
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
}

轮转数组

leetcode

//强烈推荐使用,可以使用%求余数的方式让他们进行数组元素的顺位移动
public void rotate(int[] nums, int k) {
    int n = nums.length;
    int[] newArr = new int[n];
    for (int i = 0; i < n; ++i) {
        newArr[(i + k) % n] = nums[i];
    }
    System.arraycopy(newArr, 0, nums, 0, n);
}

合并两个有序链表

leetcode

//双指针,这里用到双指针是因为需要返回头结点
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    //虚拟一个假的头指针出来,后面逻辑中就可以不需要判断null
    ListNode head = new ListNode(0), curr = head;
    while (list1 != null && list2 != null) {
        if (list1.val > list2.val) {
            curr.next = list2;
            list2 = list2.next;
        } else {
            curr.next = list1;
            list1 = list1.next;
        }
        curr = curr.next;
    }

    curr.next = list1 == null ? list2 : list1;
    return head.next;
}

//采取递归的方式处理
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    if (list1 == null) {
        return list2;
    } else if (list2 == null) {
        return list1;
    } else if (list1.val < list2.val) {
        list1.next = mergeTwoLists(list1.next, list2);
        return list1;
    } else {
        list2.next = mergeTwoLists(list1, list2.next);
        return list2;
    }       
}

合并两个有序的数组

leetcode


//每次都使用数组复制的方式,直接操作原数组,时间复杂度偏高
public void merge(int[] nums1, int m, int[] nums2, int n) {
    int mIndex = 0, nIndex = 0;
    while (mIndex < m + n && nIndex < n) {
        if (nums1[mIndex] > nums2[nIndex]) {
            System.arraycopy(
                nums1, mIndex, nums1, mIndex + 1, m + n - mIndex - 1
            );
            nums1[mIndex] = nums2[nIndex];
            nIndex++;
        }
        mIndex++;
    }

    for (; nIndex < n; nIndex++) {
        nums1[nIndex + m] = nums2[nIndex];
    }
}

//使用一个临时数组将数据代替进去
public void merge(int[] nums1, int m, int[] nums2, int n) {
    if (m == 0) {
        System.arraycopy(nums2, 0, nums1, 0, m + n);
    }
    if (n == 0) {
        return;
    }

    int[] sort = new int[m + n];
    int sortIndex = 0, mIndex = 0, nIndex = 0
    for (; sortIndex < m + n; sortIndex++) {
        if (mIndex >= m || nIndex < n && nums1[mIndex] > nums2[nIndex]) {
            sort[sortIndex] = nums2[nIndex++];
        } else {
            sort[sortIndex] = nums1[mIndex++];
        }
    }
    System.arraycopy(sort, 0, nums1, 0, m + n);
}

//使用双指针倒序的方式
public void merge(int[] nums1, int m, int[] nums2, int n) {
    if (m == 0) {
        System.arraycopy(nums2, 0, nums1, 0, m + n);
        return;
    }
    if (n == 0) {
        return;
    }
    
    int index = m + n - 1, mIndex = m - 1, nIndex = n - 1;
    for (; index >= 0; index--) {
        if (nIndex < 0 || mIndex >= 0 && nums1[mIndex] > nums2[nIndex]) {
            nums1[index] = nums1[mIndex--];
        } else {
            nums1[index] = nums2[nIndex--];
        }
    }
}

加一

leetcode

//特殊处理全为9,需要进位1的情况
public int[] plusOne(int[] digits) {
    for (int i = digits.length - 1; i >= 0; i--) {
        if ((digits[i] + 1) % 10 == 0) {
            digits[i] = 0;
        } else {
            digits[i] = digits[i] + 1;
            return digits;
        }
    }
    int[] result = new int[digits.length + 1];
    result[0] = 1;
    return result;
}

总结

  1. 双指针前后夹逼、双指针步调追踪,常常用于解决数组和链表的问题,一般都会有比较明显的特点,比如有序的数组、乘最多水的容器(优先移动低的方向指针),用双指针来降低时间复杂度
  2. 使用 map 缓存,常常用于需要二次遍历的地方,可以先缓存起来,然后再次用0(1)的哈希表去找数据