第1题
实现一个循环队列。
没什么好说的,看基本功(但是我太紧张了,实现对了,面试官让我测试一下,我测试的时候写了个死循环,唉命苦)。我使用的链表实现的,既有单链表也有双链表。
public class QueueTest {
private DNode head;
private DNode tail;
public QueueTest() {
head = new DNode(-1);
tail = head;
head.pre = tail;
head.next = tail;
}
public void push(int val) {
DNode DNode = new DNode(val);
tail.next = DNode;
DNode.pre = tail;
tail = DNode;
tail.next = head;
head.pre = tail;
}
public boolean isEmpty() {
return head == tail;
}
public void pop() {
if (!isEmpty()) {
head.next = head.next.next;
head.next.pre = head;
tail = head.pre;
}
}
public int peek() {
if (!isEmpty()) {
return head.next.data;
}
return -1;
}
public String toString() {
if (isEmpty()) return "[]";
StringBuilder sb = new StringBuilder();
sb.append("[");
for (DNode p = head.next; p != head; p = p.next) {
sb.append(p.data);
sb.append(", ");
}
sb.deleteCharAt(sb.lastIndexOf(","));
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
// QueueTest queue = new QueueTest();
// System.out.println(queue.isEmpty());
// queue.push(1);
// System.out.println(queue.isEmpty());
// System.out.println(queue);
// queue.push(2);
// System.out.println(queue);
//
// queue.push(3);
// System.out.println(queue);
//
// DNode p = queue.head.next;
// while (p != queue.head) {
// System.out.println(p.data);
// p = p.next;
// }
// System.out.println(p.next.data);
// while (!queue.isEmpty()) {
// System.out.println(queue.peek());
// queue.pop();
// }
QueueDemo que = new QueueDemo();
System.out.println(que.isEmpty());
que.push(1);
System.out.println(que);
que.push(2);
System.out.println(que);
que.push(3);
System.out.println(que);
System.out.println(que.peek());
System.out.println(que.isEmpty());
que.pop();
System.out.println(que.peek());
que.pop();
System.out.println(que.peek());
que.pop();
System.out.println(que.peek());
System.out.println(que.isEmpty());
System.out.println(que);
}
}
class DNode {
public int data;
public DNode next;
public DNode pre;
public DNode(int val) {
data = val;
next = null;
pre = null;
}
}
class Node {
int val;
Node next;
Node(int val) {
this.val = val;
}
}
class QueueDemo {
Node head;
Node tail;
QueueDemo() {
head = new Node(-1);
head.next = head;
tail = head.next;
}
public boolean isEmpty() {
return head == tail;
}
public void push(int val) {
Node node = new Node(val);
tail.next = node;
node.next = head;
tail = node;
}
public int peek() {
if (!isEmpty()) {
return head.next.val;
}
return -1;
}
public void pop() {
if (!isEmpty()) {
head.next = head.next.next;
if (head.next == head) tail = head;
}
}
public String toString() {
if (isEmpty()) return "[]";
StringBuilder sb = new StringBuilder();
sb.append("[ ");
Node p = head.next;
while (p != head) {
sb.append(p.val);
sb.append(", ");
p = p.next;
}
sb.deleteCharAt(sb.lastIndexOf(","));
sb.append("]");
return sb.toString();
}
}
第2题
计算字符串表达式的值:里面有括号,加号,和乘号,就这三中操作符。
因为最近在看jvm,知道jvm是用操作数栈实现的,但是我没有细想,只想到了用中缀表达式来求解。但是这个平时没接触过,所以吭哧了半个小时也没搞定,总之一个小时两道题,我是一道也没做出来。说难吧,这也不难啊,但是短时间想完美解决,平时肯定是要做过这种题的。昨晚在床上想想想,最后给想通了,今天上午就给实现了。
- 即先将字符串表达式转换为一个中缀表达式二叉树,叶子结点是操作数,分支节点是操作符。
这一步要计算每一个操作符的优先级,找优先级最低的,如果遇到左括号,则括号中的操作符优先级直接提升一个base(10),遇到右括号降一个base(10),这样括号内的操作符一定大于括号外的,操作符被包裹的括号层数越多,其优先级越高。
找到优先级最低的作为根,然后将字符串表达式一分为二,继续递归建树,
如果表达式中没有操作符了,说明这就是一个叶子结点,然后将其周围的括号扒掉,求其值; - 然后计算表达式树的值。如果中缀表达式树已经建好,这一步就比较简单了,就是如果是叶子结点直接返回其值;
如果是分支结点,递归求其左子树的值和右子树的值,然后看该分支的操作符是加还是乘,求对应的结果返回即可
代码:
public class StringToResultTest {
//先递归建一个中缀表达式树
public TreeNode toInTree(String string) {
//找到树的根。根不能在括号里面,并且优先级最低
int base = 0;//基础优先级,没遇到一个左括号,加10,遇到一个右括号-10
int root_ind = -1;//记录优先级最低的操作符,将其作为根
int root_p = Integer.MAX_VALUE;
for (int i = 0; i < string.length(); i++) {
char ch = string.charAt(i);
int pror = 0;
if (ch == '(') base += 10;
if (ch == ')') base -= 10;
if (ch == '+') {
pror = base + 1;//计算该字符的优先级
if (pror < root_p) {
root_ind = i;
root_p = pror;
}
}
if (ch == '*') {
pror = base + 2;//计算该字符的优先级
if (pror < root_p) {
root_ind = i;
root_p = pror;
}
}
}
if (root_ind != -1) {//说明找到了一个根,其位置在root_ind,这是一个分支结点
char op = string.charAt(root_ind);
int val = op == '+' ? 0 : 1;//加是0,乘法是1
TreeNode root = new TreeNode(val);
root.left = toInTree(string.substring(0, root_ind));
root.right = toInTree(string.substring(root_ind+1));
return root;
} else { //说明没有根了,也就是没有表达式,则将该字符串求值即可,这是一个叶子结点
//剥除左括号
int l = string.lastIndexOf('(');
if (l != -1) {
string = string.substring(l+1);
}
//剥除右括号
int r = string.indexOf(')');
if (r != -1) {
string = string.substring(0, r);
}
return new TreeNode(Integer.valueOf(string));
}
}
public int toResult(TreeNode root) {
if (root != null) {
if (root.left == null && root.right == null) {//叶子结点肯定是操作数
return root.val;
}else {
int left_ = toResult(root.left);
int right_ = toResult(root.right);
if (root.val == 0) {
return left_ + right_;
}else return left_ * right_;
}
}
return 0;
}
public static void main(String[] args) {
StringToResultTest re = new StringToResultTest();
String string = "((5*2)+10)*(30+50*10+(2*5+10))";
TreeNode root = re.toInTree(string);
System.out.println(re.toResult(root));
}
}
class TreeNode {
int val;//记录值,如果是叶子结点,就代表该结点的值;如果是分支结点,就表示操作符,0:+;1:*
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
left = null;
right = null;
}
}
上面的方法有一个缺点就是复杂度有点高,差的时候表达式树深接近n,每一次都要遍历一遍字符串求根,复杂度又是n,所以是n平方,好的时候也得nlogn。
当然了还有一种是使用双栈,一个操作数栈,一个操作符栈。双栈的做法只需要遍历一遍字符串,复杂度仅为n。
思路:
- 遇到一个操作数就进操作数栈;
- 遇到一个
(
,就直接进操作符栈; - 遇到一个
)
,就开始依次弹出操作符栈中的操作符,直到遇到第一个(
并将其弹出为止; - 遇到一个
+
,则开始依次弹出操作符栈中的操作符,直到遇到第一个(
为止,然后将+
进操作符栈; - 遇到一个
×
,则开始依次弹出操作符栈中的操作符,直到遇到一个+
或(
为止(或者这么说吧,因为栈中也就它们三个,如果栈顶也是乘号,就弹出直到栈顶不是乘号为止),然后将×
入操作符栈。 - 每一次弹出一个操作符,都取出操作数栈顶的两个数,进行计算,并保存到操作数栈顶中;
- 最后操作符栈为空,操作数栈仅剩一个数,这个数就是最后的结果。
代码:
public int toResult(String expression) {
Stack<Integer> temp = new Stack<>();//保存中间结果的操作数栈
//操作符栈,其中存的是操作符的优先级。
//0表示右括号
//1表示加号
//2表示乘号
//3表示左括号
Stack<Integer> oper = new Stack<>();
//保存对应的优先级
Map<Character, Integer> priority = new HashMap<>();
priority.put(')', 0);
priority.put('+', 1);
priority.put('*', 2);
priority.put('(', 3);
boolean flag = true;//表明开始了一个新的数字子串,因为我想实现不仅仅只能支持个位数的计算
int start = 0;
for (int i = 0; i < expression.length(); i++) {
char ch = expression.charAt(i);
if (priority.containsKey(ch)) {//操作符
if (!flag) {//说明前面的子串是操作数
flag = true;//意味着可能出现新的操作数
temp.push(Integer.valueOf(expression.substring(start, i)));
System.out.println(Integer.valueOf(expression.substring(start, i)));
}
int p = priority.get(ch);
if (p == 0) {//优先级最低,它会一直让操作符栈里的操作符出栈直到遇到左括号为止
while (oper.peek() != 3) {
int op = oper.pop();
int a = temp.pop();
int b = temp.pop();
int resl = op == 1 ? a + b : a * b;
temp.push(resl);
}
oper.pop();
}else if (p == 1) {//加号
while (!oper.empty() && oper.peek() != 3) {
int op = oper.pop();
int a = temp.pop();
int b = temp.pop();
int resl = op == 1 ? a + b : a * b;
temp.push(resl);
}
oper.push(1);
}else if (p == 2) {//乘号
while (!oper.empty() && oper.peek() == 2) {//只有当栈顶同样是乘号时才出栈计算
oper.pop();
int a = temp.pop();
int b = temp.pop();
int resl = a * b;
temp.push(resl);
}
oper.push(2);
}else {//左括号直接进栈
oper.push(3);
}
}else {//出现操作数,我们需要判断操作数的位数,将其截取出来
if (flag) {//开始出现新的操作数
flag = false;
start = i;//记录操作数的起点
}
}
}
if (!flag) {//说明还有最后一个操作数
temp.push(Integer.valueOf(expression.substring(start)));
}
while (!oper.empty()) {
int op = oper.pop();
int a = temp.pop();
int b = temp.pop();
int resl = op == 1 ? a + b : a * b;
temp.push(resl);
}
return temp.pop();
}
第3题
刚刚我同学面的,45分钟一道题。给定一个字符串表示的一元一次方程,方程只有一个未知量x,方程里面没有括号,仅仅有加号和减号。
我刚做了一下,还行,搞出来了。
例如方程:5x-3+5-4x=4-6x-8+3x
思路:
- 首先肯定是合并同类项了。我将一个带符号的数字或者x项看成是一个完整的单元,则上式可以分为
5x, -3, +5, -4x, =4, -6x, -8, +3x
这些单元,可以看到,单元(除了最后一个)的结尾都是以一个加减等号结束的; - 因此我先确定了单元的结尾,然后逆序往前找单元的开头,发现单元(除了第一个)的开头也是一个加减等号;
- 当确定了单元之后,就根据单元是x项或是常数项来进行合并同类项的操作。
- 在进行系数或常数项值的解析的时候,要注意有一个项是以=开头的。然后如果是x或者-x这种“没有系数”的也要注意一下;
- 还有就是最后一项要特别对待。
代码:
/**
* 解一个只含有加减和x的方程
* @param func 方程的表达式
* @return 解方程的结果
*/
public double solveFunc(String func) {
int count_x = 0;//x的系数
int num = 0;//方程等号右边的数,方程结果为 x * count_x = num;
Set<Character> oper = new HashSet<>();
oper.add('-');
oper.add('+');
oper.add('=');
boolean isEquals = false;//标记等号是否出现
for (int i = 0; i < func.length(); i++) {
char ch = func.charAt(i);
//因为发现每一个单元(除了最后一个)都是以加减等分割的,所以先求出分割符,然后逆方向找到单元的开头
//这样就确定了一个单元,然后再对单元的具体情况进行分析
if (oper.contains(ch) || i == func.length()-1) {//出现了分割符,或者到了最后一个单元
int end = i == func.length()-1 ? i+1 : i;//最后一个单元要特殊一点,假定以length长度为结尾
int start = end - 1;
//找到单元的开头。单元的开头要么是三种符号种的一个,要么就是第一个单元,可能没有符号
while (start > -1 && !oper.contains(func.charAt(start))) start--;
if (start == -1) start = 0;
String ele = func.substring(start, end);
System.out.println(ele);
//找到单元后开始解析单元
//单元是x表达式
if (ele.endsWith("x")) {
int val = 0;
if (ele.charAt(0) == '=') {
String sub = ele.substring(1, ele.length()-1);
//解析系数值
if (sub.equals("")) val = 1;
else if (sub.equals("+")) val = 1;
else if (sub.equals("-")) val = -1;
else val = Integer.valueOf(sub);
}else {
String sub = ele.substring(0, ele.length()-1);
//解析系数值
if (sub.equals("")) val = 1;
else if (sub.equals("+")) val = 1;
else if (sub.equals("-")) val = -1;
else val = Integer.valueOf(sub);
}
System.out.println(val);
if (isEquals) count_x -= val;
else count_x += val;
System.out.println(count_x);
}else {
//单元也可能是数字表达式
int val = 0;
if (ele.charAt(0) == '=') {
//解析系数值
val = Integer.valueOf(ele.substring(1));
}else {
val = Integer.valueOf(ele);
}
System.out.println(val);
if (isEquals) num += val;
else num -= val;
System.out.println(num);
}
if (ch == '=') isEquals = true;//出现了等号,等号右边的则需要变号
System.out.println("是否出现了等号: " + isEquals);
}
}
System.out.println(count_x + "x = " + num);
if (count_x == 0) return Double.MAX_VALUE;
else return (double) num / count_x;
}
总结
总的来说,腾讯出的题确实有水平,第一题是考察基本功的,正常情况下,都应该能做出来;
第2题也不是难的让你没有思路(不像力扣上面的难题,没有思路不看答案压根就想不出来也做不出来),这道题就是那种有了思路觉得简单但是越想细节越多,再加上时间紧张,就容易考虑步骤出错的那种。如果平时做过这种题,可能会心理上轻松一些,做起来更顺手。
字符串,数组,链表,以及栈队列,还有二叉树算是最容易考的了。反而比较令人望而生畏的图,考的较少。