线性表
线性表就是数据排成线一样的结构。
数组(Array)(ArrayList同理 )
定义:数组是在内存中存储相同数据类型的连续的空间,数组长度一旦声明,不可改变不可追加.
图示:
代码:
public static void main(String[] args) {
String[] realMan = new String[8];
realMan[0] = "宋江";
realMan[1] = "晁盖";
}
时间复杂度:
-
插入/删除:O(n)
(1+2+3+…+n)/n = n(n+1)/2n = n/2 + 1/2 = O(n)
-
查询
- 按value(值)查询:O(n)
- 按index(下标)查询:O(1)优点:查询快
缺点:增删慢面试题:
排序:
链表
定义:链表是一种物理存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
- 单向链表
图示:
时间复杂度:- 插入/删除给定值:O(1)
- 查询:O(n)
- 双向链表(Double linked list)
空间换时间
应用:LinkedHashMap
定义:双向链表是在单链表的每个节点中,再设置一个指向其前驱节点的指针。
时间复杂度:
删除:- 删除某个给定值:O(n)
- 删除给定节点:O(1)
- 插入:O(1)
- 查询效率也比单项链表高。双向链表能记住上次查找的位置。
- 循环链表
知识点:
1.记住指针next的位置
2.加入哨兵(dummy)节点:因为在删除节点时不能保证不删除头结点,当删除头结点后,就丢失了指向下一个node的指针。所以要设置一个哨兵节点记录头结点的指针。
面试题:
问答题:
-
数组与链表的区别
答:结构方面:从结构上来说,数组和链表都属于线性结构,何为线性结构,即每个元素串连,除头尾元素,每一个元素都有一个前趋、一个后继。
内存方面:从内存上来说,数组是有序的占用一块连续的内存区(‘天生线性’),而链表在内存中是分散的,并且链表是由指针连接(‘指针连接’),并且访问链表元素只能由头节点指针,依次查找,链表不存在下标,这也就导致两者在查找、添加、删除操作时间复杂度上的差异。
- 查找:访问上来说,因为数组是有序存储,切有下标,所以在随机访问中,arr[2]和arr[1000],消耗时间也是一样得;
但是链表没有下标,在访问中只能通过头节点元素指针依次访问,所以访问list[2]需要通过头节点元素指针向下查找,list[1000]需要通过依次访问前面999个元素指针顺序访问,才能找到list[1000],所以数组和链表在时间复杂度上分别是O(1)和O(n),数组-‘随机访问’;链表-‘顺序访问’。
- 添加,因为数组在内存中是连续排列,在数组添加元素,所添加元素位置之后的所有元素都要后移,下标和顺序后移一位;
链表在内存中是分散的,无序也没有下标,所以在链表中添加一个元素只是在这个节点在内存中的指针发生改变,其他元素保持不变。
- 删除和添加同理
除以上在结构和内存上的异同,两者在操作系统的内存管理上也存在差异,在内存预读方面,内存管理会将连续的存储空间读入缓存,因为数组和链表内存上的不同,所以数组往往被读入缓存,利于提高访问效率,但是链表往往不会被读入缓存,对于原本访问效率就低的链表再读入缓存效率就更低了。在实际应用中,因为链表带来的动态扩容的便利性,在做为算法的容器方面,用的更普遍一点。
- 查找:访问上来说,因为数组是有序存储,切有下标,所以在随机访问中,arr[2]和arr[1000],消耗时间也是一样得;
编程题:
- 反转链表
代码在git上
题目:输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
图示:
代码:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null){
return null;
}
ListNode pre = head;
ListNode cur = head.next;
pre.next = null;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
-
翻转链表II
题目:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。说明:
1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
图示:
代码:
public static ListNode reverseBetween(ListNode head, int m, int n) {
if (head == null || m >= n) {
// head==null 或者 m>= n 的时候,无需翻转链表
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
head = dummy;
for (int i = 1; i < m; i++) {
head = head.next;
}
// 这里要用head给其他元素赋值,否则可能出现空指针异常
ListNode prevM = head;
ListNode mNode = head.next;
ListNode nNode = mNode;
ListNode postN = nNode.next;
// nNode => prev;
//postN => current
// 2 4
for(int i = m; i < n; i++) {
/**
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
**/
ListNode next = postN.next;
postN.next = nNode;
nNode = postN;
postN = next;
}
mNode.next = postN;
prevM.next = nNode;
return dummy.next;
}
- 深度拷贝带随机指针的链表
题目: 复制带随机指针的链表
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
实现1:
图示:
代码:
public Node copyRandomList(Node head) {
if(head == null) {
return null;
}
Map<Node, Node> map = new HashMap<Node, Node>();
Node newHead = head;
while (newHead != null) {
if (!map.containsKey(newHead)) {
Node node = new Node(newHead.val);
map.put(newHead, node);
}
if (newHead.random != null) {
Node random = newHead.random;
if (!map.containsKey(random)) {
Node copyRandom = new Node(random.val);
map.put(random, copyRandom);
}
map.get(newHead).random = map.get(random);
}
newHead = newHead.next;
}
newHead = head;
while (newHead != null) {
Node next = newHead.next;
map.get(newHead).next = map.get(next);
newHead = newHead.next;
}
return map.get(head);
}
实现2:
图示:
代码:
/*
// 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 null;
}
copy(head);
copyRandom(head);
return split(head);
}
public void copy(Node head) {
Node node = head;
while(node != null) {
Node copy = new Node(node.val);
copy.next = node.next;
node.next = copy;
node = copy.next;
}
}
public void copyRandom(Node head) {
Node node = head;
while(node != null && node.next != null) {
if (node.random != null) {
node.next.random = node.random.next;
}
node = node.next.next;
}
}
public Node split(Node head) {
Node result = head.next;
Node move = head.next;
while(head != null && head.next != null) {
head.next = head.next.next;
head = head.next;
if (move != null && move.next != null) {
move.next = move.next.next;
move = move.next;
}
}
return result;
}
}
- 链表相加
题目:链表相加
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
图示:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
int carry = 0;
ListNode head = new ListNode(-1);
ListNode pre = head;
while (l1 != null && l2 != null) {
int number = l1.val + l2.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
l1 = l1.next;
l2 = l2.next;
}
while (l1 != null) {
int number = l1.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
l1 = l1.next;
}
while (l2 != null) {
int number = l2.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
l2 = l2.next;
}
if (carry != 0) {
ListNode node = new ListNode(carry);
pre.next = node;
}
return head.next;
}
}
- 实现LRU缓存机制(面试高频)
题目:LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
图示:
代码:
class LRUCache {
private class CacheNode {
CacheNode prev;
CacheNode next;
int key;
int value;
public CacheNode(int key, int value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
private int capacity;
private Map<Integer, CacheNode> valNodeMap = new HashMap();
private CacheNode head = new CacheNode(-1, -1);
private CacheNode tail = new CacheNode(-1, -1);
public LRUCache(int capacity) {
this.capacity = capacity;
tail.prev = head;
head.next = tail;
}
public int get(int key) {
if (!valNodeMap.containsKey(key)) {
return -1;
}
CacheNode current = valNodeMap.get(key);
current.prev.next = current.next;
current.next.prev = current.prev;
moveToTail(current);
return valNodeMap.get(key).value;
}
public void put(int key, int value) {
if (get(key) != -1) {
valNodeMap.get(key).value = value;
return;
}
if (valNodeMap.size() == capacity) {
valNodeMap.remove(head.next.key);
head.next = head.next.next;
head.next.prev = head;
}
CacheNode insert = new CacheNode(key, value);
valNodeMap.put(key, insert);
moveToTail(insert);
}
private void moveToTail(CacheNode current) {
current.prev = tail.prev;
tail.prev = current;
current.prev.next = current;
current.next = tail;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
栈
定义:栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
特征:后进先出(LIFO-last in first out):最后插入的元素最先出来。
图示:
经典面试题:
- 利用栈,进行括号验证:
说明:
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
代码
public static Boolean isValid(String s){
if(s == null || s.length() == 0){
return true;
}
Stack<Character> stack = new Stack();
for (char c:s.toCharArray()) {
// 注意下面判断不可以用"(".equals(c) 这种写法,char不属于String
if('(' == c || '[' == c || '{' == c){
stack.push(c);
}else{
if(')'== c){
if(stack.isEmpty() || stack.pop() != '('){
return false;
}
}else if(']'==c){
if(stack.isEmpty() || stack.pop() != '['){
return false;
}
}else if('}'==c){
if(stack.isEmpty() || stack.pop() != '{'){
return false;
}
}
}
}
if(stack.isEmpty()){
return true;
}else{
return false;
}
}
- 最小栈:
题目:
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。
示例
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
图示:
代码:
class MinStack {
/** initialize your data structure here. */
Stack<Integer> minStack;
Stack<Integer> stack;
public MinStack() {
minStack = new Stack();
stack = new Stack();
}
public void push(int x) {
stack.push(x);
if(minStack.isEmpty() || x < minStack.peek()){
minStack.push(x);
}else{
minStack.push(minStack.peek());
}
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
- 区间最大值:
题目:
图示:
代码:
import java.util.Stack;
public class ByteDanceEx1 {
public static void main(String[] args) {
int[] numbers = {5,2,3,4,1};
System.out.println(new ByteDanceEx1().getMax(numbers));
}
public int getMax(int[] numbers) {
if (numbers == null || numbers.length == 0) {
return 0;
}
Stack<Integer> stack = new Stack<>();
int max = 0;
//求前缀和数组
int[] sum = new int[numbers.length + 1];
for (int i = 1; i <= numbers.length; i++) {
sum[i] = sum[i - 1] + numbers[i - 1];
}
for(int i = 0; i < numbers.length; i++) {
while (!stack.isEmpty() && numbers[i] < numbers[stack.peek()]) {
int index = stack.pop();
int left = i;
int right = i;
if (stack.isEmpty()) {
left = 0;
} else {
left = index;
}
//用 O(1)取到前缀和
max = Math.max(max, numbers[index] * (sum[right] - sum[left]));
}
stack.push(i);
}
while (!stack.isEmpty()) {
int index = stack.pop();
int left = numbers.length;
int right = numbers.length;
if (stack.isEmpty()) {
left = 0;
} else {
left = index;
}
max = Math.max(max, numbers[index] * (sum[right] - sum[left]));
}
return max;
}
}
队列
定义:队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
特征:先进先出(FIFO-first in first out):最先插入的元素最先出来。
图示:
经典面试题:
问答题:
-
栈和队列的区别
栈是一种数据结构,是只能在某一端插入和删除的特殊线性表。
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底;栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈,删除则称为退栈。 栈也称为先进后出表。队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
在队列这种数据结构中,最先插入在元素将是最先被删除;反之最后插入的元素将最后被删除,因此队列又称为“先进先出”的线性表。
队列(queue)在计算机科学中,是一种先进先出的线性表。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
编程题:
- 用栈实现队列 点我
题目:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
图示:
代码:
class MyQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
/** Initialize your data structure here. */
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
/** Push element x to the back of queue. */
public void push(int x) {
stack1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if(empty()) {
return -1;
}
if (stack2.isEmpty()) {
while(!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
/** Get the front element. */
public int peek() {
if(empty()) {
return -1;
}
if (stack2.isEmpty()) {
while(!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
- 推荐结果打散 (快手面试题)
题目:
图示:
package gupao;
import java.util.List;
import java.util.ArrayList;
import java.util.Queue;
import java.util.LinkedList;
public class KuaiShouEx2 {
public static void main(String[] args) {
List<String> picAndVideoList = new ArrayList<>();
picAndVideoList.add("v_0");
picAndVideoList.add("v_1");
picAndVideoList.add("v_2");
picAndVideoList.add("p_3");
picAndVideoList.add("p_4");
picAndVideoList.add("p_5");
picAndVideoList.add("v_6");
picAndVideoList.add("p_7");
picAndVideoList.add("v_8");
picAndVideoList.add("v_9");
List<String> result = new KuaiShouEx2().getRecommendenResult(picAndVideoList, 4);
for (int i = 0; i < result.size(); i++) {
System.out.println(result.get(i));
}
}
public List<String> getRecommendenResult(List<String> picAndVideo, int maxInterval) {
List<String> result = new ArrayList<>();
if (picAndVideo == null || picAndVideo.size() == 0) {
return result;
}
Queue<String> videoQueue = new LinkedList<>();
Queue<String> picQueue = new LinkedList<>();
boolean firstPic = false;
int index = 0;
int picAndVideoSize = picAndVideo.size();
while (!firstPic && index < picAndVideoSize) {
if (isVideo(picAndVideo.get(index))) {
result.add(index, picAndVideo.get(index));
index++;
} else {
firstPic = true;
}
}
while (index < picAndVideoSize) {
if (isVideo(picAndVideo.get(index))) {
videoQueue.add(picAndVideo.get(index));
} else {
picQueue.add(picAndVideo.get(index));
}
index++;
}
int currentSize = result.size();
while(!videoQueue.isEmpty() && !picQueue.isEmpty()) {
if (currentSize >= maxInterval) {
result.add(picQueue.poll());
currentSize = 0;
} else {
result.add(videoQueue.poll());
currentSize++;
}
}
while(!videoQueue.isEmpty()) {
result.add(videoQueue.poll());
}
if(currentSize >= maxInterval && !picQueue.isEmpty()) {
result.add(picQueue.poll());
}
return result;
}
public boolean isVideo(String clip) {
if(clip.indexOf("v") != -1) {
return true;
}
return false;
}
}