目录
时间复杂度分析:
递归
题1:爬楼梯
解法1:递归
解法2:循环
题2:两数之和
解法1:暴力枚举
解法2: 哈希表
题3:合并两个有序数组
解法1:直接合并后排序
解法2:双指针
解法3:逆向双指针
题4:移动零
解法1:双指针两次遍历
解法2:双指针一次遍历
题5:找出所有数组中消失的数字
解法1:哈希表
解法2:原地修改
题6:合并两个有序链表
解法1:循环加双指针
解法2:递归
题7:删除排序链表中的重复元素
解法一:一次遍历
解法2:递归
题8:环形链表
解 :快慢指针
题9:环形链表 II
编辑解法:快慢指针
题10:相交链表
解法一:双指针
解法二:双指针plus
题11:反转链表
解法一:迭代
题12:回文链表
解法一:双指针
解法二:反转+快慢指针
题13:链表的中间结点
编辑 解法一:两次遍历
解法二:快慢指针
题14:给定一个链表,删除链表中倒数第n个结点
编辑解法一:两次遍历
解法二:快慢指针
只关注循环执行次数最多的一段代码;
总复杂度等于最高阶项的复杂度;
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。
常见时间复杂度:
O(1) 常数阶
O(N) 线性阶
O() 平方阶
O() 对数阶
O() 线性对数阶
O() 立方阶
O() 指数阶
O(N!) 阶乘阶
从小到大依次是:
O(1) < O() < O(N) < O() < O() < O() < O() < O(N!) < O()
1.一个问题的解可以分为几个子问题的解
2.这个问题与分解之后的子问题,除了问题规模不同,求解思路完全一样
3.存在基线/中止条件。
当满足以上3点是,就可以考虑使用递归来解决问题。
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
当n=1时,只有一种方法;当n=2时,有一次爬一阶和一次性爬两阶两种方法;
当n=3时,有三种方法,一次爬一阶、一阶两阶、两阶一阶;
可以发现,n阶台阶的方法,就是n-1阶与n-2阶方法的和。
n=1,n=2已知,这里假设n=6。
要计算n=6,就是计算n=5加n=4;
要计算n=5,就是计算n=3加n=4;
要计算n=4,就是计算n=3加n=2;
这里n=4,n=3会多次运算,会增大时间复杂度,
解决方法就是,定义一个HashMap,需要计算某个值时,先看看map里面有没有,如果有直接取,如果没有,在计算,然后存到map里面。
这样做,避免了重复运算,加快计算速度,减小时间复杂度。
class Solution {
private HashMap hashMap = new HashMap<>();
public int climbStairs(int n) {
if (n == 1)
return 1;
if (n == 2)
return 2;
if (hashMap.get(n) != null)
return hashMap.get(n);
else{
int result = climbStairs(n-1) + climbStairs(n - 2);
hashMap.put(n,result);
return result;
}
}
}
运行结果:
因为n=1和n=2很容易计算,我直接自底向上解决问题。
如图所示,f(1) + f(2) =f(3);
f(2) + f(3) =f(4);以此类推,只到求出f(n)。
public int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
int result = 0;
//计算后面的值,就是前一个加前前一个
//first保存前前一个的值
//second保存前一个的值
int first = 1;
int second = 2;
for(int i = 3; i <=n ; i++){
result = first + second;
first = second;
second = result;
}
return result;
}
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] arr = new int[2];
for(int i = 0;i
仔细观察暴力枚举法,就会发现,很多数重复参与比较。
如果不让那些数重复比较,就会减小时间复杂度。
方法也很简单,只要一个哈希表,当参加比较的数不符合条件时,将它存到哈希表中,这样只要一个for循环就ok,时间复杂度为O(n)。
class Solution {
public int[] twoSum(int[] nums, int target) {
Map map = new HashMap<>();
int n = nums.length;
int[] arr = new int[2];
for(int i = 0;i<=n;i++){
int result = target - nums[i];
Integer resultIndex = map.get(result);
if(resultIndex!=null){
arr[0] = resultIndex;
arr[1] = i;
break;
}else{
map.put(nums[i],i);
}
}
return arr;
}
}
给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。
请你 合并 nums2
到 nums1
中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1
中。为了应对这种情况,nums1
的初始长度为 m + n
,其中前 m
个元素表示应合并的元素,后 n
个元素为 0
,应忽略。nums2
的长度为 n
。
从题目可以知道,num1的长度刚好可以放下num2数组。
所以,我们可以将num2直接放进num1的尾部,然后对整个数组进行排序。
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < n; i++) {
nums1[m+i]=nums2[i];
}
Arrays.sort(nums1);
}
效率很低,时间复杂度是O((m+n)log(m+n))
因为num1和num2已经是排序好的数组,我们可以利用这一点,从而省去排序的步骤,就可以减小时间复杂度。
利用双指针,指向两个数组的头部,比较两个元素的大小,每次将最小的元素取出,存进一个临时数组,最后将临时数组赋值给num1就ok了。
//双指针
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] temp = new int[m+n];
int p = 0;
int q = 0;
//每次比较的最小值
int l = 0;
while(p < m || q < n){
if(p == m){
l = nums2[q];
q++;
}else if(q == n){
l = nums1[p];
p++;
}else if(nums1[p]
时间复杂度O(m+n),因为两个数组都循环了一次
空间复杂度:O(m+n)
解法2中空间复杂度 O(m+n),是因为需要一个长度为m+n的临时数组。
仔细观察题目,就会发现,num1后面有几个元素是0,也就是空,刚好可以存入num2的元素,利用这一点,优化双指针。
我们可以从两个数组的最后哟个元素开始遍历,将最大的元素存入num1中为0的位置即可。
//逆向双指针
public void merge(int[] nums1, int m, int[] nums2, int n){
int p = m-1;
int q = n-1;
int len = m+n-1;
while(p>=0||q>=0){
if(p < 0){
nums1[len--] = nums2[q--];
}else if(q < 0){
nums1[len--] = nums1[p--];
}else if(nums1[p] > nums2[q]){
nums1[len--] = nums1[p--];
}else{
nums1[len--] = nums2[q--];
}
}
}
时间复杂度为O(m+n),
空间复杂度为O(1),因为是在原地修改数组,不需要额外空间。
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作
定义两个指针,都指向数组头部元素,开始遍历数组。
如果第i个元素为0,i++;
如果第i个元素不为0,将第i个元素赋值给第j个元素,i++,j++;
直到最后一个元素。
最后将j之后的元素都赋值成0就ok了。
public void moveZeroes(int[] nums) {
if(nums==null) {
return;
}
//双指针
int i = 0;
int j = 0;
int len = nums.length;
for(;i
时间复杂度:O(n)
空间复杂度:O(1)
定义两个指针,开始时都指向数组的头部元素。
参考快速排序的思想:这里我们以0为中心点,把不等于0的放到左边,等于0的放到右边。
从头遍历,只要i处元素不等于0,就交换j处与i处的元素。
public void moveZeroes(int[] nums) {
if(nums==null) {
return;
}
//双指针一次遍历
int i = 0;
int j = 0;
int len = nums.length;
for(;i
时间复杂度:O(n)
空间复杂度:O(1)
给你一个含 n
个整数的数组 nums
,其中 nums[i]
在区间 [1, n]
内。请你找出所有在 [1, n]
范围内但没有出现在 nums
中的数字,并以数组的形式返回结果。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1] 输出:[5,6]
示例 2:
输入:nums = [1,1] 输出:[2]
进阶:你能在不使用额外空间且时间复杂度为 O(n)
的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
我们可以使用哈希表,将数组的元素都存储到哈希表中,然后遍历数组,判断元素是否出现在哈希集合中。
public List findDisappearedNumbers(int[] nums) {
Set map = new HashSet();
List list = new ArrayList<>();
for (int num : nums) {
map.add(num);
}
for (int i = 1;i<=nums.length;i++){
if(!map.contains(i)){
list.add(i);
}
}
return list;
}
时间复杂度:O(n)
空间复杂度:O(n)
不满足要求!!!
题目意思就是不让用哈希表,那我们可以模仿哈希表,在原地修改。
仔细看题,数组长度为n,数组中元素刚好在[1,n]这个范围内。
这样做,遍历数组,每遇到一个数x,我们就让nums[x-1]增加n或将其改变成对应的负数,
遍历一遍后,这个数组中不大于n或者不是负数的数的下标加1就是那个消失的数字。
注意:由于nums[x]处的元素可能被修改过,我们需要对其还原。
(nums[x] - 1)%n
public List findDisappearedNumbers(int[] nums) {
int n = nums.length;
List list = new ArrayList<>();
for(int num:nums){
//还原它本来的值
int x = (num - 1) % n;
nums[x] += n;
}
for(int i = 0; i < n; i++){
if(nums[i] <= n){
list.add(i + 1);
}
}
return list;
}
时间复杂度:O(n)
空间复杂度:O(1)
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
提示:
[0, 50]
-100 <= Node.val <= 100
l1
和 l2
均按 非递减顺序 排列给的链表递增排列。
定义一个节点p,定义两个指针分别指向两个链表的头节点,比较大小,第一次,谁小就把谁接到p节点后面,对应的指针向后挪一位,继续比较,依次往后面接,直到有一个指针指向空。
这个时候,将剩下的那个链表直接接到另一个链表后面。
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
ListNode p = new ListNode(0);
ListNode pre = p;
if(list1 == null) return list2;
if(list2 == null) return list1;
while(list1 != null && list2!= null){
if(list1.val < list2.val){
pre.next = list1;
list1 = list1.next;
}else{
pre.next = list2;
list2 = list2.next;
}
pre = pre.next;
}
if(list1 == null){
pre.next = list2;
}
if(list2 == null){
pre.next = list1;
}
return p.next;
}
}
空间复杂度:O(1)
递归就是自己调用自己,而且要有终止条件。
先判断头结点,谁小谁就指向其余结点的合并结果,这里的终止条件就是某个链表为空,也就是说链表已经合并完成。
递归这玩意给人的感觉就是只可意会不可言传
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
if(list1.val < list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}else{
list2.next = mergeTwoLists(list2.next,list1);
return list2;
}
}
空间复杂度:O(m+n)
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
从链表头结点开始,依次向后遍历,判断当前结点的值是否等于下一个结点的值,如果相等,让当前结点直接指向下下一个结点,直到遍历完整个链表。
public ListNode deleteDuplicates(ListNode head) {
if(head == null){
return head;
}
ListNode h = head;
while(h.next != null){
if(h.val == h.next.val){
h.next = h.next.next;
}else{
h = h.next;
}
}
return head;
}
空间复杂度:O(1)
当处理完一个结点后,剩下的结点处理方式与第一个相同。
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
return head.val == head.next.val ? head.next : head;
}
时间复杂度:O(n)
空间复杂度:O(n)
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
我们定义两个指针,一个快指针,一个慢指针。然后让指针往后遍历链表,如果两个指针相遇了,就说明有环。
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode fast = head;
ListNode slow = head;
while(fast.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
时间复杂度:O(n)
空间复杂度:O(1)
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
上一题让我们确定环是否存在,我们定义两个快慢指针,如果相遇,则说明环存在。
现在,让我们确定入环的第一个结点,思路跟上一题差不多,只不过多加了一个判断。
首先,利用上一题的解题思路判断环是否存在,如果存在,我们让慢指针指向头结点,然后让快慢指针的速度相同,如果它两再次相遇,就找到了入环时的第一个结点。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) return null;
ListNode fast = head;
ListNode slow = head;
boolean flag = false;
while(fast.next != null && fast.next.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
flag = true;
break;
}
}
//环存在
if(flag){
//慢指针指向头结点
slow = head;
while(slow != fast){
//慢指针和快指针移动速度一样
slow = slow.next;
fast = fast.next;
}
return slow;
}
return null;
}
}
空间复杂度:O(1)
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为 0
listA
- 第一个链表listB
- 第二个链表skipA
- 在 listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在 listB
中(从头节点开始)跳到交叉节点的节点数评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
定义两个指针,都依次往后遍历链表,如果两个链表长度相同,那么当两个指针指向同一个结点时,就是他们的相交的起始结点。
如果长度不相同,当短的那条链表指向null时,让他重新指向另外一条链表的头结点,继续遍历,按照这个思路,两个指针总会相遇,相遇即找到相交的起始结点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){ return null;}
ListNode a = headA;
ListNode b = headB;
while(a != b){
if(a == null){
a = headB;
}else{
a = a.next;
}
if(b == null){
b = headA;
}else{
b = b.next;
}
}
return a;
}
空间复杂度:O(1)
我们首先遍历A链表,遍历B链表得到两个链表的长度,然后相减得到差值d。
接下来,我们让长的那个链表先移动d个结点,这是两个链表的剩余长度就相同,当两个指针指向同一个结点时,就找到了相交的起始链表。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){ return null;}
ListNode a = headA;
ListNode b = headB;
int alen = 0;
int blen = 0;
int d = 0;
while(a != null){
a = a.next;
alen+=1;
}
while(b != null){
b = b.next;
blen += 1;
}
if(alen>blen){
a = headA;
b = headB;
d = alen - blen;
}else{
a = headB;
b = headA;
d = blen - alen;
}
for(int i = 0;i
空间复杂度:O(1)
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
我们要反转链表,就是让链表第一个结点成为最后一个结点,最后一个结点成为第一个结点。
从头开始遍历链表,让当前结点指向上一个结点,因为头结点的没有前一个结点,所以我们要定义一个preNode=null存储前一个结点,还要存储后一个结点。
public ListNode reverseList(ListNode head) {
ListNode preNode = null;
ListNode curr = head;
while(curr!=null){
ListNode next = curr.next;
curr.next = preNode;
preNode = curr;
curr = next;
}
return preNode;
}
空间复杂度:O(1)
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
进阶:你能否用 O(n)
时间复杂度和 O(1)
空间复杂度解决此题?
将链表的元素赋值到数组中,然后利用双指针判断是否回文。
/**
* 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 boolean isPalindrome(ListNode head) {
List list = new ArrayList<>();
ListNode curr = head;
while(curr != null){
list.add(curr.val);
curr = curr.next;
}
int len = list.size();
int left = 0;
int right = len - 1;
while(left < right){
if(list.get(left).equals(list.get(right))){
left++;
right--;
}else{
return false;
}
}
return true;
}
}
空间复杂度:O(n)
题目进阶要求,空间复杂度为O(1)。
所以我们直接在链表内部判断是否回文。
我们先利用快慢指针找到链表中心点,然后将中心点之后的元素反转,在判断前部分元素和后半部分元素的反转结果是否相同就ok了。
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
if(fast != null){
slow = slow.next;
}
slow = reverse(slow);
fast = head;
while(slow != null){
if(slow.val != fast.val){
return false;
}
slow = slow.next;
fast = fast.next;
}
return true;
}
//反转链表
public ListNode reverse(ListNode head){
ListNode pre = null;
ListNode curr = head;
while(curr != null){
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
}
空间复杂度:O(1)
给你单链表的头结点 head
,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
第一次遍历得到链表的长度,除以2 +1,就是中间结点所在的位置,第二次遍历就可以找到中间结点。
/**
* 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 middleNode(ListNode head) {
ListNode h = head;
int len = 0;
while(h != null){
h = h.next;
len++;
}
h = head;
int size = (len / 2) ;
while(size > 0){
h = h.next;
size--;
}
return h;
}
}
空间复杂度:O(1)
定义两个指针,一个快,一个慢,当快指针指向最后一个结点或者null时,慢指针指向的就是中间结点。
/**
* 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 middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
时间复杂度:O(n)
空间复杂度:O(1)
给定一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
进阶:能尝试使用一趟扫描实现吗?
第一次遍历得到链表的长度k,要求删除倒数第n个结点,反过来,删除第k-n+1个结点。
第二次遍历删除。
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0,head);
ListNode cur = dummy;
int k = 0;
ListNode h = head;
while(h != null){
h = h.next;
k++;
}
int len = k - n + 1;
for(int i = 1;i
时间复杂度:O(n)
空间复杂度:O(1)
题目进阶要求,一次遍历。
定义两个指针,快指针先移动n-1次,然后两个指针同时移动,当快指针移动到最后一个结点时,慢指针所在的结点就是要删除的结点。
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0,head);
ListNode fast = head;
ListNode slow = dummy;
while(n>0){
fast = fast.next;
n--;
}
while(fast!=null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
ListNode result = dummy.next;
return result;
}
}
时间复杂度:O(n)
空间复杂度:O(1)