面试题20、21、22、23

面试题20.表示数值的字符串

在这里插入图片描述
有限状态自动机

起初,这个自动机处于「初始状态」。随后,它顺序地读取字符串中的每一个字符,并根据当前状态和读入的字符,按照某个事先约定好的「转移规则」,从当前状态转移到下一个状态;当状态转移完成后,它就读取下一个字符。当字符串全部读取完毕后,如果自动机处于某个「接受状态」,则判定该字符串「被接受」;否则,判定该字符串「被拒绝」。

用「当前处理到字符串的哪个部分」当作状态的表述,则本题目的所有状态:
0.起始的空格
1.符号位,如 +/-
2.整数部分
3.左侧有整数的小数点 如 3.
4.左侧无整数的小数点(根据前面的第二条额外规则,需要对左侧有无整数的两种小数点做区分)
5.小数部分
6.字符e或E
7.指数部分的符号位
8.指数部分的整数部分
9.末尾的空格

根据题意,「初始状态」应当为状态 0,而「接受状态」的集合则为状态 2、状态 3、状态 5、状态 8 以及状态 9。换言之,字符串的末尾要么是空格,要么是数字,要么是小数点,但前提是小数点的前面有数字。

在这里插入图片描述

1、初始化:

  • 1.状态转移表 states:设 states[i],其中 i 为所处状态,states[i] 使用哈希表存储可转移至的状态。键值对 (key, value) 含义:若输入 key ,则可从状态 i 转移至状态 value 。
  • 当前状态 p:起始状态初始化为 p=0

2、状态转移循环:遍历字符串 s 的每个字符 c

  • 1.记录字符类型 t:

    • 当 c 为正负号时,执行 t = s;
    • 当 c 为数字时,执行 t = d;
    • 当 c 为 e,E 时,执行 t = e;
    • 当 c 为 . 或 空格时,执行 t = . 或 空格;
    • 否则,执行 t = ?,代表为不属于判断范围的非法字符,后续直接返回 false。
  • 2.终止条件:若字符类型 t 不在哈希表 states[p] 中,说明无法转移至下一状态,因此直接返回 false。

  • 3.状态转移:状态 p 转移至 states[p][t]

3、返回值:跳出循环后,若状态 p ∈ 2,3,5,8,9 说明结尾合法,返回 true,否则返回 false

class Solution {
    public boolean isNumber(String s) {
    	Map[] states = {
			new HashMap<>() {{put(' ', 0); put('s', 1); put('d', 2); put('.', 4);}},//0状态
			new HashMap<>() {{put('d', 2); put('.', 4); }}, //1状态
			new HashMap<>() {{put('d', 2); put('.', 3); put('e', 6); put(' ', 9);}},//2状态
			new HashMap<>() {{put('d', 5); put('e', 6); put(' ', 9); }}, //3.
			new HashMap<>() {{put('d', 5); }}, //4.
			new HashMap<>() {{put('d', 5); put('e', 6); put(' ', 9); }}, //5.
			new HashMap<>() {{put('s', 7); put('d', 8); }}, //6.
			new HashMap<>() {{put('d', 8); }}, //7.
			new HashMap<>() {{put('d', 8); put(' ', 9); }}, //8.
			new HashMap<>() {{put(' ', 9); }}  //9.
		};
		int p = 0;
		char t;
		for(char c : s.toCharArray()) {
			if(c >= '0' && c <= '9') t = 'd';
			else if(c == '+' || c == '-') t = 's';
			else if(c == 'e' || c == 'E') t = 'e';
			else if(c == '.' || c == ' ') t = c;
			else t = '?';
			//判断 p 状态中是否有这一变化
			if(!states[p].containsKey(t)) return false;
			p = (int) states[p].get(t);
		}
		return p == 2 || p == 3 || p == 5 || p == 8 || p == 9;
    }
}
  • 时间复杂度 O(N) : 其中 N 为字符串 s 的长度,判断需遍历字符串,每轮状态转移的使用 O(1) 时间。

常规思路:

class Solution {
    public boolean isNumber(String s) {
		if(s == null || s.length() == 0) return false; //s为空对象或 s长度为0(空字符串)时, 不能表示数值
		boolean isNum, isDot, ise_or_E; //标记是否遇到数位、小数点、‘e’或'E'
		char[] str = s.trim().toCharArray(); //删除字符串头尾的空格,转为字符数组,方便遍历判断每个字符
		for(int i = 0; i < str.length; i++) {
			if(str[i] >= '0' && str[i] <= '9') isNum = true; //判断当前字符是否为 0~9 的数位
			else if(str[i] == '.') { //遇到小数点
				if(isDot || ise_or_E) return false; //小数点之前可以没有整数,但是不能重复出现小数点、或出现‘e’、'E'
				isDot = true; //标记已经遇到小数点
			}
			else if(str[i] == 'e' || str[i] == 'E') { //遇到‘e’或'E'
				if(!isNum || ise_or_e) return false; //‘e’或'E'前面必须有整数,且前面不能重复出现‘e’或'E'
				ise_or_E = true; //标记已经遇到 ‘e’ 或‘E’
				isNum = false; //重置isNum,因为‘e’或'E'之后也必须接上整数,防止出现 123e或者123e+的非法情况
			}
			else if(str[i] == '-' || str[i] == '+') {
				//正负号只可能出现在第一个位置,或者出现在‘e’或'E'的后面一个位置
				if(i != 0 && str[i - 1] != 'e' && str[i - 1] != 'E') return false;
			}
			else return false; //其他情况均为不合法字符
		}
		return isNum; // isNum为true,说明前面全是数字,或者是合法的数值
    }
}

————————————————————————————————————————

面试题21.调整数组顺序使奇数位于偶数前面

在这里插入图片描述

首尾双指针

  • 定义头指针 left,尾指针 right
  • left 一直往右移,直到它指向的值为偶数
  • right 一直往左移,直到它指向的值为奇数
  • 交换 nums[left] 和 nums[right]
  • 重复上述操作,直到 left == right
class Solution {
    public int[] exchange(int[] nums) {
    	int len = nums.length;
		int left = 0;
		int right = len - 1;
		while(left < right) {
			while(left < right && nums[left] % 2 == 1) left++;
			while(left < right && nums[right] % 2 == 0) right--;
			if(left < right) {
				int temp = nums[left];
				nums[left] = nums[right];
				nums[right] = temp;
				left++;
				right--;
			}
		}
		return nums;
    }
}
  • 时间复杂度 O(N): N 为数组 nums 长度,双指针 i,j 共同遍历整个数组。
  • 空间复杂度 O(1) : 双指针 i,j 使用常数大小的额外空间。

快慢双指针

  • 定义快慢指针 fast、slow 都指向头节点
  • slow 指向奇数存放的位置
  • fast 负责搜索奇数,如果找到奇数则与 slow 指向的数进行交换
  • fast 指向数组末尾说明算法结束

在这里插入图片描述

class Solution {
    public int[] exchange(int[] nums) {
		int fast = 0, slow = 0;
		while(fast < nums.length) {
			if(nums[fast] % 2 == 1) { //fast指向奇数,交换
				int temp = nums[fast];
				nums[fast] = nums[slow];
				nums[slow] = temp;
				slow++; //交换完毕,slow后移
			}
			fast++;
		}
		return nums;
    }
}

————————————————————————————————————————

面试题22.链表中倒数第k个节点

在这里插入图片描述
快慢双指针

1、初始化:前指针 former 、后指针 latter,双指针都指向头节点

2、构建双指针距离:前指针 former 先向前走 k 步(结束后,双指针 former 和 latter 之间相距 k 步)

3、双指针共同移动:循环中,双指针 former 和 latter 每轮都向前走一步,直至 former 走过链表尾节点时跳出(跳出后, latter 与尾节点距离为 k-1,即 latter 指向倒数第 k 个节点)。
在这里插入图片描述

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
    	ListNode former = head, latter = head;
    	for(int i = 0; i < k; i++) former = former.next;
    	while(former != null) {
    		former = former.next;
    		latter = latter.next;
    	}
    	return latter;
    }
}
  • 时间复杂度 O(N) : N 为链表长度;总体看, former 走了 N 步, latter 走了 (N−k) 步。
  • 空间复杂度 O(1) : 双指针 former , latter 使用常数大小的额外空间。

————————————————————————————————————————

面试题23.链表中环的入口节点

在这里插入图片描述
在这里插入图片描述

常规方法

使用 Set 集合,遍历链表,把每个节点都存入 Set 中,若出现重复,则此节点为入口节点

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null) return null;
        Set<ListNode> set = new HashSet<>();
        ListNode colon = head;
        int i = 0;
        while(colon != null) {
            if(set.contains(colon)) return colon;
            set.add(colon);
            colon = colon.next;
        }
        return null;
    }
}

双指针

1.判断链表是否有环

可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的

那么为什么fast指针和slow指针一定会相遇呢?这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。


2.如果有环,如何找到这个环的入口

假设从头结点到环形入口节点 的节点数为 x。

环形入口节点到 fast指针与slow指针相遇节点 节点数为 y。

从相遇节点 再到环形入口节点节点数为 z。

在这里插入图片描述
那么相遇时:

  • slow指针走过的节点数为: x + y
  • fast指针走过的节点数: x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数

而因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2

  • (x + y)* 2 = x + y + n(y + z)

因为我们要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以我们要求x ,将x单独放在左面:x = n (y + z) - y

再从 n(y+z) 中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针

当 n为1的时候,公式就化解为 x = z,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null) return null;
        ListNode fast = head;
        ListNode slow = head;
        while(true) {
            if(fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) break; //当两者第一次相遇
        }
        fast = head; //一个从头节点出发,一个从相遇节点出发。当两者相遇时,就是入口节点
        while(fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
}
  • 时间复杂度 O(N):慢指针先走至环内与快指针相遇,再从相遇点走至入口节点,总共走了 N 个节点,故总体为线性复杂度。
  • 空间复杂度 O(1) :双指针使用常数大小的额外空间。

证明文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值