剑指 Offer 20. 表示数值的字符串
题目:请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、“5e2”、"-123"、“3.1416”、"-1E-16"、“0123"都表示数值,但"12e”、“1a3.14”、“1.2.3”、"±5"及"12e+5.4"都不是。
解题思路一:
/*确定有限状态自动机[思路一]
预备知识:
1、确定有限状态自动机(以下简称[自动机])是一类计算模型。它包含一系列状态,这些状态中:
(1)有一个特殊的状态,被称作「初始状态」。
(2)还有一系列状态被称为[接受状态],它们组成了一个特殊的集合。其中,一个状态可能既是[初始状态],也是[接受状态]。
2、起初,这个自动机处于[初始状态]。
随后,它顺序地读取字符串中的每一个字符,并根据当前状态和读入的字符,按照某个事先约定好的[转移规则],从当前状态转移到下一个状态; 当状态转移完成后,它就读取下一个字符。当字符串全部读取完毕后,如果自动机处于某个[接受状态],则判定该字符串[被接受];
否则,判定该字符串[被拒绝]。
注意:
1、如果输入的过程中某一步转移失败了,即不存在对应的[转移规则],此时计算将提前中止。在这种情况下我们也判定该字符串[被拒绝]。
2、一个自动机,总能够回答某种形式的[对于给定的输入字符串S,判断其是否满足条件P]的问题。在本题中,条件P即为[构成合法的表示数值的字符串]。
3、自动机驱动的编程,可以被看做一种暴力枚举方法的延伸:它穷尽了在任何一种情况下,对应任何的输入,需要做的事情。
4、自动机在计算机科学领域有着广泛的应用。在算法领域,它与大名鼎鼎的字符串查找算法「KMP」算法有着密切的关联;在工程领域,它是实现[正则表达式]的基础。
问题描述:
1、在C++文档中,描述了一个合法的数值字符串应当具有的格式。具体而言,它包含以下部分:
(1)符号位,即 ++、-− 两种符号
(2)整数部分,即由若干字符 0-90−9 组成的字符串
(3)小数点
(4)小数部分,其构成与整数部分相同
(5)指数部分,其中包含开头的字符e(大写小写均可)、可选的符号位,和整数部分
2、相比于 C++ 文档而言,本题还有一点额外的不同,即允许字符串首末两端有一些额外的空格。
3、在上面描述的五个部分中,每个部分都不是必需的,但也受一些额外规则的制约,如:
(1)如果符号位存在,其后面必须跟着数字或小数点。
(2)小数点的前后两侧,至少有一侧是数字。
思路与算法:
1、根据上面的描述,现在可以定义自动机的[状态集合]了。那么怎么挖掘出所有可能的状态呢?一个常用的技巧是,用「当前处理到字符串的哪个部分」当作状态的表述。根据这一技巧,不难挖掘出所有状态:
(1)起始的空格
(2)符号位
(3)整数部分
(4)左侧有整数的小数点
(5)左侧无整数的小数点(根据前面的第二条额外规则,需要对左侧有无整数的两种小数点做区分)
(6)小数部分
(7)字符e
(8)指数部分的符号位
(9)指数部分的整数部分
(10)末尾的空格
2、下一步是找出[初始状态]和[接受状态]的集合。
根据题意,[初始状态]应当为状态1;
[接受状态]的集合则为状态 3、状态 4、状态 6、状态 9 以及状态 10。
换言之,字符串的末尾要么是空格,要么是数字,要么是小数点,但前提是小数点的前面有数字。
3、最后,需要定义[转移规则]。结合数值字符串应当具备的格式,将自动机转移的过程以图解的方式表示出来:
/*
4、比较上图与「预备知识」一节中对自动机的描述,可以看出有一点不同:
(1)我们没有单独地考虑每种字符,而是划分为若干类。
由于全部1010 个数字字符彼此之间都等价,因此只需定义一种统一的「数字」类型即可。
对于正负号也是同理。
5、在实际代码中,我们需要处理转移失败的情况。例如当位于状态1(起始空格)时,没有对应字符e的状态。
为了处理这种情况,我们可以创建一个特殊的拒绝状态。
如果当前状态下没有对应读入字符的[转移规则],我们就转移到这个特殊的拒绝状态。
一旦自动机转移到这个特殊状态,我们就可以立即判定该字符串不[被接受].
复杂度分析:
时间复杂度:O(N),其中N为字符串的长度。我们需要遍历字符串的每个字符,其中状态转移所需的时间复杂度为O(1)。
空间复杂度:O(1)。只需要创建固定大小的状态转移表。
*/
解题思路二
/*思路二:
解题思路:
本题使用有限状态自动机。根据字符类型和合法数值的特点,先定义状态,再画出状态转移图,最后编写代码即可。
字符类型:
空格[ ]、数字[0—9]、正负号[+-]、小数点[.]、幂符号[eE]。
状态定义:
按照字符串从左到右的顺序,定义以下 9 种状态。
0.开始的空格
1.幂符号前的正负号
2.小数点前的数字
3.小数点、小数点后的数字
4.当小数点前为空格时,小数点、小数点后的数字
5.幂符号
6.幂符号后的正负号
7.幂符号后的数字
8.结尾的空格
结束状态:
合法的结束状态有 2, 3, 7, 8 。
*/
/*
算法流程:
1、初始化:
状态转移表states:设 states[i],其中i为所处状态,states[i]使用哈希表存储可转移至的状态。键值对(key, value)含义:若输入 key ,则可从状态i转移至状态value。
当前状态p:起始状态初始化为p=0 。
2、状态转移循环:遍历字符串s的每个字符c。
(1)记录字符类型t:分为四种情况。
[1]当c为正负号时,执行 t = 's' ;
[2]当c为数字时,执行 t = 'd' ;
[3]当c为e,E时,执行 t = 'e' ;
[4]当c为.,空格时,执行t=c即用字符本身表示字符类型);
[5]否则,执行t ='?',代表为不属于判断范围的非法字符,后续直接返回false。
(2)终止条件:若字符类型t不在哈希表states[p]中,说明无法转移至下一状态,因此直接返回False 。
(3)状态转移:状态p转移至states[p][t]。
3、返回值:跳出循环后,若状态p∈2,3,7,8 ,说明结尾合法,返回True,否则返回False。
复杂度分析:
时间复杂度O(N):其中N为字符串s的长度,判断需遍历字符串,每轮状态转移的使用O(1)时间。
空间复杂度O(1):states和p使用常数大小的额外空间。
*/
实现代码:
//思路一代码实现:
class Method1{
public boolean isNumber(String s) {
Map<State, Map<CharType, State>> transfer = new HashMap<State, Map<CharType, State>>();
Map<CharType, State> initialMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_SPACE, State.STATE_INITIAL);
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
put(CharType.CHAR_SIGN, State.STATE_INT_SIGN);
}};
transfer.put(State.STATE_INITIAL, initialMap);
Map<CharType, State> intSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
}};
transfer.put(State.STATE_INT_SIGN, intSignMap);
Map<CharType, State> integerMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_POINT, State.STATE_POINT);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_INTEGER, integerMap);
Map<CharType, State> pointMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_POINT, pointMap);
Map<CharType, State> pointWithoutIntMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
}};
transfer.put(State.STATE_POINT_WITHOUT_INT, pointWithoutIntMap);
Map<CharType, State> fractionMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_FRACTION, fractionMap);
Map<CharType, State> expMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SIGN, State.STATE_EXP_SIGN);
}};
transfer.put(State.STATE_EXP, expMap);
Map<CharType, State> expSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
}};
transfer.put(State.STATE_EXP_SIGN, expSignMap);
Map<CharType, State> expNumberMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_EXP_NUMBER, expNumberMap);
Map<CharType, State> endMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_END, endMap);
int length = s.length();
State state = State.STATE_INITIAL;
for (int i = 0; i < length; i++) {
CharType type = toCharType(s.charAt(i));
if (!transfer.get(state).containsKey(type)) {
return false;
} else {
state = transfer.get(state).get(type);
}
}
return state == State.STATE_INTEGER || state == State.STATE_POINT || state == State.STATE_FRACTION || state == State.STATE_EXP_NUMBER || state == State.STATE_END;
}
public CharType toCharType(char ch) {
if (ch >= '0' && ch <= '9') {
return CharType.CHAR_NUMBER;
} else if (ch == 'e' || ch == 'E') {
return CharType.CHAR_EXP;
} else if (ch == '.') {
return CharType.CHAR_POINT;
} else if (ch == '+' || ch == '-') {
return CharType.CHAR_SIGN;
} else if (ch == ' ') {
return CharType.CHAR_SPACE;
} else {
return CharType.CHAR_ILLEGAL;
}
}
//枚举状态
enum State {
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END,
}
//枚举字符的类型
enum CharType {
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_SPACE,
CHAR_ILLEGAL,
}
}
/*
思路二的代码实现:
Java 的状态转移表states使用Map[]数组存储。
*/
class Method2{
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', 5); put(' ', 8); }}, // 2.
new HashMap<>() {{ put('d', 3); put('e', 5); put(' ', 8); }}, // 3.
new HashMap<>() {{ put('d', 3); }}, // 4.
new HashMap<>() {{ put('s', 6); put('d', 7); }}, // 5.
new HashMap<>() {{ put('d', 7); }}, // 6.
new HashMap<>() {{ put('d', 7); put(' ', 8); }}, // 7.
new HashMap<>() {{ put(' ', 8); }} // 8.
};
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 = '?';
if(!states[p].containsKey(t)) return false;
p = (int)states[p].get(t);
}
return p == 2 || p == 3 || p == 7 || p == 8;
}
}
剑指 Offer 24. 反转链表
题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000
/*
方法一:迭代
假设链表为1→2→3→∅,我们想要把它改∅←1←2←3。
在遍历链表时,将当前节点的next指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。
在更改引用之前,还需要存储后一个节点。
最后返回新的头引用。
复杂度分析:
时间复杂度:O(n),其中n是链表的长度。需要遍历链表一次。
空间复杂度:O(1)。
*/
class Method1{
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
/*
方法二:递归
递归版本稍微复杂一些,其关键在于反向工作。假设链表的其余部分已经被反转,现在应该如何反转它前面的部分?
假设链表为:
n1→…→nk−1→nk→nk+1→…→nm→∅
若从节点nk+1到nm已经被反转,而我们正处于nk。
n1→…→nk−1→nk→nk+1←…←nm
我们希望nk+1的下一个节点指向nk。
所以,nk.next.next=nk。
需要注意的是n1的下一个节点必须指向∅。如果忽略了这一点,链表中可能会产生环。
复杂度分析:
时间复杂度:O(n),其中n是链表的长度。需要对链表的每个节点进行反转操作。
空间复杂度:O(n),其中n是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为n层。
*/
class Method2{
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;
}
}
总结:
最近在整理前端生态体系和JAVA的生态体系的知识点,以及UI常用的知识点和服务器端运维所需要了解的知识点!当然如果以后还有时间的话还准备去系统的学习一下软件工程和软件测试所需要学习的知识!数据结构和算法顺便在写博客的时候顺便学习一下!数据结构和算法最核心的是思路,而不是代码!现在有许多小伙伴都会存在一个误区!都觉得越学越简单~~~!因为各种各样的框架让我们写代码更快速更高效!殊不知渐渐地陷入了一个陷阱:只是为了学习框架而学习框架!一旦离了框架就无从下手了!因此,当我们学习一个框架时,一定要想明白这个框架到底解决了什么样的问题!为什么要去学习它?以及能否不使用框架来解决这个问题!时间一长就会形成自己的一种自己独到的见解!然后就可以自己尝试着去写一些Demo自己造轮子了!加油!各位小伙伴们!