2021年经典大厂面试题——算法与数据结构版(含答案)
经典大厂面试题
题目
- 请问,Object作为HashMap的key的话,对Object有什么要求吗?
- 请问 hashset 存的数是有序的吗?
- 输入一个二叉树和一个整数,打印出二叉树中节点值的和等于输入整数所有的路径
- 二叉树的搜索区间
- 现在有一个单向链表,谈一谈,如何判断链表中是否出现了环
- 随机链表的复制
- 找出数组中和为S的一对组合,找出一组就行
- 求一个数组中连续子向量的最大和
- 谈一谈,如何得到一个数据流中的中位数?
- 你知道哪些排序算法,这些算法的时间复杂度分别是多少,解释一下快排?
- 请你解释一下,内存中的栈(stack)、堆(heap) 和静态区(static area) 的用法。
- 说一说,heap和stack有什么区别。
- 请你设计一个算法,用来压缩一段URL?
- 谈一谈,id全局唯一且自增,如何实现?
- 一个长度为N的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数,请说一下你的思路并手写代码
- 请问求第k大的数的方法以及各自的复杂度是怎样的,另外追问一下,当有相同元素时,还可以使用什么不同的方法求第k大的元素
- 判断一个链表是否为回文链表,说出你的思路并手写代码
答案
请问,Object作为HashMap的key的话,对Object有什么要求吗?
考察点:哈希表
参考回答: 要求 Object 中 hashcode 不能变。
解读: 什么是hashcode。Java语言中,Object对象有个特殊的方法:hashcode(), hashcode()表示的是JVM虚拟机为这个Object对象分配的一个int类型的数值,JVM会使用对象的hashcode值来提高对HashMap、Hashtable哈希表存取对象的使用效率。
关于Object对象的hashCode()返回值,网上对它就是一个简单的描述:“JVM根据某种策略生成的”.
什么是JVM。Java虚拟机(Java Virtual Machine 简称JVM)。通常来说,高级语言运行在不同的平台上,需要编译为不同的语言,但是如果不同的平台都有虚拟机,由虚拟机来执行Java语言,就不需要编译为不同的语言,这也就解释了为什么Java可以跨平台运行。
请问 hashset 存的数是有序的吗?
参考回答: Hashset 是无序的。
解读:什么事hashset。hashset储存的是唯一的对象,没有下标,只能通过增强for,或者迭代器进行遍历。当出现相同的对象,会合并,只储存一个空间。
//遍历方法一:
Iterator<news> it=set.iterator();
while(it.hasNext()){
News n=it.next();
}
-----------------------------------------
//遍历方法二:
for(News n:set){
printf(n);
}
输入一个二叉树和一个整数,打印出二叉树中节点值的和等于输入整数所有的路径
剑指Offer JZ24 二叉树中和为某一值的路径
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
vector<int> path;
vector<vector<int> >res;
if(root==NULL)return res;
Find(root,path,expectNumber,res);
return res;
}
void Find(TreeNode* root,vector<int>& p,int sum,vector<vector<int>> &res){
p.push_back(root->val);
if(sum==root->val&&!root->left&&!root->right){
res.push_back(p);
}
if(root->left)Find(root->left,p,sum-root->val,res);
if(root->right)Find(root->right,p,sum-root->val,res);
//回溯
p.pop_back();
}
};
二叉树的搜索区间
给定两个值 k1 和 k2(k1 < k2)和一个二叉查找树的根节点。找到树中所有值在 k1 到 k2 范围内的节点。即打印所有x (k1 <= x <= k2) 其中 x 是二叉查找树的中的节点值。返回所有升序的节点值。
利用中序遍历,得到节点值递增的数组。在从数组中选择满足条件的数。
现在有一个单向链表,谈一谈,如何判断链表中是否出现了环
LeetCode.141.环形链表
方法一:哈希
最容易想到的方法是
遍历所有节点,
每次遍历到一个节点时,判断该节点此前是否被访问过。
可以使用哈希表来存储所有已经访问过的节点。
每次到达一个节点的时候
判断:
如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。
重复这一过程,直到我们遍历完整个链表即可
方法二:快慢指针
- 设置一个快慢指针fast和slow,开始时slow和fast都指向链表的头节点head。然后slow每次移动一步(slow->next),fast每次移动两步(fast->next->next),在链表中遍历。
- 如果链表无环,fast指针在移动过程一定先遇到终点,到达终点的标志是(fast->next=NULL)(奇数个节点)||(fast->next->next=NULL(偶数个节点)),直接返回false,表示链表无环。
- 如果有环slow和fast一定会相遇,并且是在NULL第一个入环的节点处相遇(证明略),slow==fast直接返回true。
bool hasCycle(struct ListNode* head) {
if (head == NULL || head->next == NULL) {
return false;
}
struct ListNode* slow = head;
struct ListNode* fast = head->next;
while (slow != fast) {
if (fast == NULL || fast->next == NULL) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
随机链表的复制
解法一: 使用hash存储原结点和克隆结点的映射关系,通过映射关系处理克隆结点的random指针。
代码一:java语言
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if(head == null){
return head;
}
// map方法,空间复杂度O(n)
Node node = head;
// 使用hash表存储旧结点和新结点的映射---key:旧节点,val:新节点
Map<Node,Node> map = new HashMap<>();
while(node != null){
Node clone = new Node(node.val,null,null);
map.put(node,clone);
node = node.next;
}
//第二次遍历
node = head;
while(node != null){
//根据key旧节点,找到val新节点,完善next指针
map.get(node).next = map.get(node.next);
//根据key旧节点,找到val新节点,完善random指针
map.get(node).random = map.get(node.random);
//遍历指针后移
node = node.next;
}
//根据旧节点,找到新节点
return map.get(head);
}
}
代码二:cpp语言
class Solution {
public:
Node* copyRandomList(Node* head) {
unordered_map<Node*, Node*> link;
Node* pNode = head;
while (pNode != nullptr) {
link[pNode] = new Node(pNode->val);
pNode = pNode->next;
}
pNode = head;
while (pNode != nullptr) {
link[pNode]->next = link[pNode->next];
link[pNode]->random = link[pNode->random];
pNode = pNode->next;
}
return link[head];
}
};
找出数组中和为S的一对组合,找出一组就行
类似LeetCode39题
LeetCode.39. 组合总和
从数组中找出所有组合为s的数
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
Deque<Integer> path = new ArrayDeque<>();
dfs(candidates, 0, len, target, path, res);
return res;
}
/**
* @param candidates 候选数组
* @param begin 搜索起点
* @param len 冗余变量,是 candidates 里的属性,可以不传
* @param target 每减去一个元素,目标值变小
* @param path 从根结点到叶子结点的路径,是一个栈
* @param res 结果集列表
*/
private void dfs(int[] candidates, int begin, int len, int target, Deque<Integer> path, List<List<Integer>> res) {
// target 为负数和 0 的时候不再产生新的孩子结点
if (target < 0) {
return;
}
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
// 重点理解这里从 begin 开始搜索的语意
for (int i = begin; i < len; i++) {
path.addLast(candidates[i]);
// 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
dfs(candidates, i, len, target - candidates[i], path, res);
// 状态重置
path.removeLast();
}
}
}
求一个数组中连续子向量的最大和
谈一谈,如何得到一个数据流中的中位数?
你知道哪些排序算法,这些算法的时间复杂度分别是多少,解释一下快排?
请你解释一下,内存中的栈(stack)、堆(heap) 和静态区(static area) 的用法。
区:专门用来保存对象的实例(new 创建的对象和数组),实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中)
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身.
3.一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
栈区:对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4.由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.
静态区/方法区:
1.方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
3.全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
附:
堆和栈是程序运行的关键,应该将其理解清楚。
栈是运行时的单位,而堆是存储时的单位。
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处)。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
说一说,heap和stack有什么区别。
请你设计一个算法,用来压缩一段URL?
谈一谈,id全局唯一且自增,如何实现?
一个长度为N的整形数组,数组中每个元素的取值范围是[0,n## 1],判断该数组否有重复的数,请说一下你的思路并手写代码
请问求第k大的数的方法以及各自的复杂度是怎样的,另外追问一下,当有相同元素时,还可以使用什么不同的方法求第k大的元素
判断一个链表是否为回文链表,说出你的思路并手写代码
回文链表具有对称性,可以用快慢指针求解。
快指针一次走两步,慢指针一次走一步,快指针指向末尾时,慢指针刚好指向链表中间,也就是前半部分链表的尾节点,返回慢指针,用慢指针获取后半分链表的头节点,并反转。具体如下:
思路
整个流程可以分为以下五个步骤:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。
我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。
步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。
代码
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode* firstHalfEnd = endOfFirstHalf(head);
ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
ListNode* p1 = head;
ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
//返回结果
return result;
}
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
ListNode* endOfFirstHalf(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
反转单链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};