数据结构与算法 ---【基础入门篇】

学习链接:https://www.bilibili.com/video/BV1HN4y1K7Rx?p=1&vd_source=6f1075d4365d16741b423bc01a0d8cd7

1、反转链表

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

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

使用两种方式解题

1. 解法1:迭代

迭代,重复某一过程,每一次处理结果作为下一次处理的初始值,这些初始值类似于状态、每次处理都会改变状态、直至到达最终状态。

思路

从前往后遍历链表,将当前节点的next指向上一个节点,因此需要一个变量存储上一个节点prev,当前节点处理完需要寻找下一个节点,因此需要一个变量保存当前节点curr,处理完后要将当前节点赋值给prev,并将next指针赋值给curr,因此需要一共变量提前保存下一个节点的指针next

在这里插入图片描述
1、将下一个节点指针保存到next变量 next = curr.next
2、将下一个节点的指针指向prev,curr.next = prev
3、准备处理下一个节点,将curr赋值给prev
4、将下一个节点赋值为curr,处理一个节点

class Solution {
    public ListNode reverseList(ListNode head) {
        // 迭代
        // 保存原前驱结点
        ListNode prev = null;
        // 保存原后继结点
        ListNode next;
        // 当前结点
        ListNode cur = head;

        while(cur!=null){
            next = cur.next;
            cur.next = prev;
            prev = cur;
            cur = next;
        }

        return prev;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)
2. 解法2:递归

递归:以相似的方法重复,类似于树结构,先从根节点找到叶子节点,从叶子节点开始遍历大的问题(整个链表反转)拆成性质相同的小问题(两个元素反转)curr.next.next = curr

将所有的小问题解决,大问题即解决

在这里插入图片描述
只需每个元素都执行curr.next.next = curr ,curr.next = null 两个步骤即可

为了保证链不断,必须从最后一个元素开始

class Solution {
    public ListNode reverseList(ListNode head) {
        // 递归
        if(head == null || head.next==null){
            return head;
        }

        // 递归到最后一个结点
        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next=null;

        return newHead;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

2、寻找数组的中心下标

寻找数组的中心下标

数组中某一个下标,左右两边的元素之后相等,该下标即为中心索引

输入:nums = [1, 7, 3, 6, 5, 6]
输出:3

1. 解法1:双指针

思路

先统计出整个数组的总和,然后从第一个元素开始叠加,总和递减当前元素,叠加递增当前元素,直到两个值相等。

class Solution {
    public int pivotIndex(int[] nums) {
        // 双指针
        // 获取数组总和
        int total = Arrays.stream(nums).sum();
        int sum = 0;

        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
            
            if(sum==total){
                return i;
            }
            total-=nums[i];
        }
        return -1;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)
2. 解法2:前缀和

思路

记数组的全部元素之和为total,当遍历到第i个元素时,设其左侧元素之和为sum,则其右侧元素之和为 total - nums[i] - sum。左右侧元素相等即为 sum = total = nums[i] - sum,即 2 * sum + nums[i] = total

class Solution {
    public int pivotIndex(int[] nums) {
        // 前缀和
        int total = Arrays.stream(nums).sum();
        int sum = 0;

        for(int i=0;i<nums.length;i++){
            if(2 * sum + nums[i] == total){
                return i;
            }
            sum+=nums[i];
        }
        return -1;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

3、删除排序数组中的重复项

删除排序数组中的重复项

一个有序数组 nums,原地删除重复出现的元素,使每个元素只出现一次,返回删除后的数组的新长度。

不能使用额外的数组空间,必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。

输入:[0,1,2,2,3,3,4]
输出:5

1. 解法1:双指针

思路

数组完成排序后,我们可以放置两个指针 ij,其中 i 是慢指针,而 j 是快指针。只要 nums[i] = nums[j],我们就增加 j 以跳过重复项。

当遇到nums[j] != nums[i] 时,跳过重复项的运动已经结束,必须把nums[j]的值赋值到nums[i+1],然后递增 i,接着再次重复相同的过程,直到 j 到达数组的末尾为止。

class Solution {
    public int removeDuplicates(int[] nums) {
        if(nums.length == 0){
            return 0;
        }

        int i = 0;
        for(int j = 1;j<nums.length; j++){
            if(nums[i] != nums[j]){
                i++;
                nums[i] = nums[j];
            }
        }
        return i+1;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

4、素数个数统计

素数个数统计

统计n以内的素数个数
素数:只能被1和自身整除的自然数,0 和 1 除外

输入:100
输出:25

1. 解法1:暴力

直接从2开始遍历,判断是否能被2到自身之间的数整除

// 暴力解法
public static int bf(int n) {
    int count = 0;
    for (int i = 2; i < n; i++) {
        count += isPrime(i) ? 1 : 0;
    }
    return count;
}

public static boolean isPrime(int x) {
    for (int i = 2; i < x; i++) {
        if (x % i == 0) {
            return false;
        }
    }
    return true;
}

其中 isPrime可以优化循环次数

public static boolean isPrime(int x){
	// 若i能被x整除,则x/i一定能被整除,因此只需判断i和根号x之中较小的即可
	for(int i = 2; i * i <= x; i++){
		if(x % i == 0){
			return false;
		}
	} 
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
2. 解法2:埃筛法
public static int eratosthenes(int n){
	// 创建一个boolean类型的数组
	boolean [] isPrime = new boolean[n];
	int ans = 0;
	for(int i = 2; i < n; i++){
		if(!isPrime[i]){
			ans += 1;
			for(int j = i * i ; j < n; j += i){
				isPrime[j] = true;
			}
		}
	}
	return ans;
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(N)

将合数标记为truej = i * i2 * i 优化而来,系数2会随着遍历递增(j += i,相当于递增了系数2),每一个合数都会有两个比本身要小的因子(0,1除外)2 * i 必然会遍历到这两个因子

当2递增到大于根号n时,其实后面的已经无需再判断(或者只需判断后面一段),而2到根号n、实际上在 i 递增的过程中已经计算过了,i 实际上就相当于根号n

例如:n = 25 会计算以下
2 * 4 = 8
3 * 4 = 12

但实际上8和12已经标记过,在n = 17时已经计算了 3 * 4,2 * 4

5、x的平方根

x的平方根

在不使用sqrt(x)的函数的情况下,得到x的平方根的整数部分。

输入:x = 4
输出:2

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。

1. 解法1:二分查找

思路

x 的平方根肯定在 0 ~ x 之间,使用二分查找定位该数字,该数字的平方一定最接近x的,m平方值如果大于x,则往左找,如果小于等于x,则往右找

找到 0 ~ x 的最中间的数m
如果 m * m > x,则mx/2 ~ x 的中间数字,直到 ,m * m <xm则为平方根的整数部分
如果 m * m <= x,则取 0 ~ x/2 的中间值,直到两边的界限重合,找到最大的整数,则为x平方根的整数部分

// 二分查找
public static int mySqrt(int x) {
    int index = -1;
    int l = 0;
    int r = x;

    while (l <= r) {
        int mid = l + (r - l) / 2;
        if ((long) mid * mid <= x) {
            index = mid;
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return  index;
}
  • 时间复杂度:O(logN)
  • 空间复杂度:O(1)
2. 解法2:牛顿迭代

假设平方根是 i ,则 i 和 x/i 必然都是x的因子,而 x/i 必然等于 i ,推导出 i + x / i = 2 * i,得出 i = (i +x / i) / 2

由此得出解法,i 可以任选一个值,只要上述公式成立,i 必然就是x的平方根,如果不成立, (i + x / i) /2得出的值进行递归,直至得出正确解

public static int newton(int x){
	if(x==0) return 0;
	return ((int)(sqrts(x,x)));
}

public static double sqrts(double i,int x){
	double res = (i + x / i) / 2;
	if(res == i){
		return i;
	}else{
		return sqets(res,x);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值