学习笔记
文章目录
1.git基本操作
# 版本号
git --version
# 创建目录
mkdir learn_git
# 查看目录内容
ls -al
# 初始化git目录
git init
# 配置用户信息
git config --global user.name "GeekUniversity"
git config --global user.email "[email protected]"
# 扩展
git config --local # 只对当前git仓库有效
git config --global # 对账户下所有git仓库有效
git config --system # 对系统里所有账户下的git仓库有效
# 查看git配置信息
git config --global --list
# 清除git配置信息
git config--unset--global 某个配置项
2.如何高效学习
-
1.5~2倍速看视频,反复暂停看难点
-
五毒神掌(敢于死记硬背
-
要看高票代码与高质量题解(国际版的高票回答
-
做过的题目确保做5遍
3.精通一个领域三步走
- 切碎知识点 imgUrl
- 刻意练习
- 过遍数<肌肉记忆>
- 练习缺陷/弱点
- 不舒服、不爽、枯燥
- 生活例子:乒乓球、台球、游戏等等
- 寻求反馈
4.训练环境配置
- 电脑设置
- Chrome默认搜索引擎设为Google
- Microsoft New Terminal
- 调快键盘响应速度
- Code Style
- Google Java Style Guide
- LeetCode.cn
- [刻意练习]编程指法
-
home/end
- 行头行尾
-
google
-
top tips vs code
-
vscode 使用技巧
-
-
选中一个单词
- Ctrl + Shift + ->
-
- 自顶向下的编程方式
- 金字塔写作方式
5.时间和空间复杂度
1.常见时间复杂度
-
O(1): Constant Complexity 常数复杂度
-
O(log n): Logarithmic Complexity 对数复杂度
-
O(n): Linear Complexity 线性时间复杂度
-
O(n^2): N square Complexity 平⽅
-
O(n^3): N square Complexity ⽴⽅
-
O(2^n): Exponential Growth 指数
-
O(n!): Factorial 阶乘
2.为什么斐波那契时间复杂度是指数级
3.思考题
- 二叉树遍历 - 前序、中序、后序:时间复杂度是多少?O(n)
- 图的遍历:时间复杂度是多少?O(n)
- 搜索算法:DFS、BFS 时间复杂度是多少?O(n)
- 二分查找:时间复杂度是多少?O(log n)
6.数组
图示
ArrayList源码
336: public boolean add(E e)
337: {
338: modCount++;
339: if (size == data.length)
340: ensureCapacity(size + 1);
341: data[size++] = e;
342: return true;
343: }
354: public void add(int index, E e)
355: {
356: checkBoundInclusive(index);
357: modCount++;
358: if (size == data.length)
359: ensureCapacity(size + 1);
360: if (index != size)
361: System.arraycopy(data, index, data, index + 1, size - index);
362: data[index] = e;
363: size++;
364: }
170: public void ensureCapacity(int minCapacity)
171: {
172: int current = data.length;
173:
174: if (minCapacity > current)
175: {
176: E[] newData = (E[]) new Object[Math.max(current * 2, minCapacity)];
177: System.arraycopy(data, 0, newData, 0, size);
178: data = newData;
179: }
180: }
时间复杂度
- lookup O(1)
- insert O(n)
- delete O(n)
Array 实战题目
-
移动零
-
盛最多水的容器
-
爬楼梯
-
三数之和
-
删除排序数组中的重复项
-
旋转数组
7.链表
双链表图示
单链表源码
class LinkedList {
Node head;
class Node {
int data;
Node next;
Node(int d) {
data = d; }
}
}
Java双向链表源码
76: public class LinkedList<T> extends AbstractSequentialList<T>
77: implements List<T>, Deque<T>, Cloneable, Serializable
78: {
82: private static final long serialVersionUID = 876323262645176354L;
83:
87: transient Entry<T> first;
88:
92: transient Entry<T> last;
93:
97: transient int size = 0;
98:
102: private static final class Entry<T>
103: {
105: T data;
106:
108: Entry<T> next;
109:
111: Entry<T> previous;
112:
117: Entry(T data)
118: {
119: this.data = data;
120: }
121: }
时间复杂度
-
lookup O(n)
-
insert O(1)
-
delete O(1)
实际运用
LRU缓存算法 https://www.jianshu.com/p/b1ab4a170c3c
实战题目
LeetCode 146. LRU缓存机制 https://leetcode-cn.com/problems/lru-cache/
8.跳表
定义
跳表(skip list)对标的是平衡树(AVL Tree)和二分查找, 是一种 插入/删除/搜索 都是 O(log n) 的数据结构。1989 年出现。
它最大的优势是原理简单、容易实现、方便扩展、效率更高。因此 在一些热门的项目里用来替代平衡树,如 Redis、LevelDB 等。
图示
时间复杂度
O(logn)
空间复杂度
O(n)
实际运用
- Redis 设计与实现 跳跃表 https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html
- 为啥 redis 使用跳表(skiplist)而不是使用 red-black?https://www.zhihu.com/question/20202931
9.栈
图示
特点
- 后进先出
- 入栈出栈 O(1)
- 查询 O(n)
应用
- 浏览器前进后退
- 括号匹配
- 表达式计算
Stack接口文档
后进先出更推荐 Deque
Deque<Integer> stack = new ArrayDeque<Integer>();
API
Stack源码
public class Stack<E> extends Vector<E> {
public Stack() {
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0) throw new EmptyStackException();
return elementAt(len - 1);
}
public boolean empty() {
return size() == 0;
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
}
push方法
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
add(obj, elementData, elementCount);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}
private Object[] grow() {
return grow(elementCount + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
capacityIncrement > 0 ? capacityIncrement : oldCapacity
/* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
pop方法
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized void removeElementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
modCount++;
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
peek方法
public synchronized E peek() {
int len = size();
if (len == 0) throw new EmptyStackException();
return elementAt(len - 1);
}
E elementData(int index) {
return (E) elementData[index];
}
Vector源码
public synchronized void addElement(E obj) {
modCount++;
add(obj, elementData, elementCount);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}
private Object[] grow() {
return grow(elementCount + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
capacityIncrement > 0 ? capacityIncrement : oldCapacity
/* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
Stack示例代码
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack);
System.out.println(stack.search(4));
stack.pop();
stack.pop();
Integer topElement = stack.peek();
System.out.println(topElement);
System.out.println(" 3的位置 " + stack.search(3));
实战题目
- 有效的括号
- 最小栈
- 柱状图中最大的矩形
- 滑动窗口最大值
10.队列
图示
普通队列
Queue接口文档
Modifier and Type | Method | Description |
---|---|---|
boolean | empty() | Tests if this stack is empty. |
E | peek() | Looks at the object at the top of this stack without removing it from the stack. |
E | pop() | Removes the object at the top of this stack and returns that object as the value of this function. |
E | push(E item) | Pushes an item onto the top of this stack. |
int | search(Object o) | Returns the 1-based position where an object is on this stack. |
offer 入队
add 入队。失败会报错
remove 出对头,返回对头
poll 出队头,返回队头,会返回null
element 查看对头
peek 查看对头,会返回null
Queue示例代码
Queue<String> queue = new LinkedList<String>();
queue.offer("one");
queue.offer("two");
queue.offer("three");
queue.offer("four");
System.out.println(queue);
String polledElement = queue.poll();
System.out.println(polledElement);
System.out.println(queue);
String peekedElement = queue.peek();
System.out.println(peekedElement);
System.out.println(queue);
while(queue.size() > 0) {
System.out.println(queue.poll());
}
11.双端队列
图示
时间复杂度
Deque接口文档
示例代码
Deque<String> deque = new LinkedList<String>();
deque.push("a");
deque.push("b");
deque.push("c");
System.out.println(deque);
String str = deque.peek();
System.out.println(str);
System.out.println(deque);
while (deque.size() > 0) {
System.out.println(deque.pop());
}
System.out.println(deque);
作业
绘制自己的数据结构和算法脑图
简单
用 add first 或 add last 这套新的 API 改写 Deque 的代码
Deque<String> deque = new LinkedList<String>();
deque.addFirst("a"); // 从头加
deque.addLast("b"); // 从尾加
deque.push("c"); // 从头加
deque.addFirst("d"); // 从头加
deque.addLast("e"); // 从尾加
deque.push("f"); // 从尾加
分析 Queue 和 Priority Queue 的源码
/*
Queue源码分析
Queue接口定义了6个方法
*/
public interface Queue<E> extends Collection<E> {
// 添加元素,成功返回true,失败报异常
boolean add(E e);
// 添加元素,成功返回true,失败返回false
boolean offer(E e);
// 取出并删除对头,如果队列空,则报异常
E remove();
// 取出并删除对头,如果队列空,则返回null
E poll();
// 取出不删除对头,如果队列空,则报异常
E element();
// 取出不删除对头,如果队列空,则返回null
E peek();
}
package com.example.demo;
import java.util.*;
import java.util.function.Consumer;
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
// 默认大小
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 队列。底层是平衡二叉树。按照自定义序或者自然序。不允许空。
// 只保证了头元素是最小的。并不保证有序。
transient Object[] queue; // transient关键字标识不会被序列化
// 队列元素个数
private int size = 0;
// 自定义比较器。空就使用自然序。
private final Comparator<? super E> comparator;
// 队列被结构性修改的次数。
transient int modCount = 0;
// 创建初始值为11的自然序队列
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 指定初始值大小
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
public PriorityQueue(Collection<? extends E> c) {
// 是SortedSet实例,就用SortedSet的比较器
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
// 是PriorityQueue实例,就用PriorityQueue的比较器
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
// 其余情况,没有自定义比较器
this.comparator = null;
initFromCollection(c);
}
}
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
public PriorityQueue(SortedSet<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
if (c.getClass() == PriorityQueue.class) {
// 队列转成数组
this.queue = c.toArray();
this.size = c.size();
} else {
initFromCollection(c);
}
}
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
// 根据给定集合初始值队列数组
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
// 构建最大堆
heapify();
}
// 虚拟机能开辟的最大数组
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 数组扩容
// 时间复杂度O(logN)
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 不足64。扩容两倍+2。超过64,扩容1.5倍。
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原来的是数组拷贝到新数组
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢出,报OOM错误
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// add底层调的offer
public boolean add(E e) {
return offer(e);
}
// 判断数组是否需要扩容,需要则调用grow犯法
// 插入元素后,可能破坏堆的平衡,调用siftUp(上滤)方法调整堆至平衡
public boolean offer(E e) {
// 添加元素为空,抛异常
if (e == null)
throw new NullPointerException();
// 修改数+1
modCount++;
int i = size;
// 个数超过数组大小,扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0) // 空数组直接加
queue[0] = e;
else
siftUp(i, e); // i数组大小,e添加元素
return true;
}
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
boolean removeEq(Object o) {
for (int i = 0; i < size; i++) {
if (o == queue[i]) {
removeAt(i);
return true;
}
}
return false;
}
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public Object[] toArray() {
return Arrays.copyOf(queue, size);
}
public <T> T[] toArray(T[] a) {
final int size = this.size;
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(queue, size, a.getClass());
System.arraycopy(queue, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
public Iterator<E> iterator() {
return new Itr();
}
// 普通数组的遍历
// 内部类Itr,利用curosr指针迭代内部数组queue
private final class Itr implements Iterator<E> {
private int cursor = 0;
private int lastRet = -1;
private ArrayDeque<E> forgetMeNot = null;
private E lastRetElt = null;
private int expectedModCount = modCount;
public boolean hasNext() {
return cursor < size ||
(forgetMeNot != null && !forgetMeNot.isEmpty());
}
public E next() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (cursor < size)
return (E) queue[lastRet = cursor++];
if (forgetMeNot != null) {
lastRet = -1;
lastRetElt = forgetMeNot.poll();
if (lastRetElt != null)
return lastRetElt;
}
throw new NoSuchElementException();
}
public void remove() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (lastRet != -1) {
E moved = PriorityQueue.this.removeAt(lastRet);
lastRet = -1;
if (moved == null)
cursor--;
else {
if (forgetMeNot == null)
forgetMeNot = new ArrayDeque<>();
forgetMeNot.add(moved);
}
} else if (lastRetElt != null) {
PriorityQueue.this.removeEq(lastRetElt);
lastRetElt = null;
} else {
throw new IllegalStateException();
}
expectedModCount = modCount;
}
}
public int size() {
return size;
}
public void clear() {
modCount++;
for (int i = 0; i < size; i++)
queue[i] = null;
size = 0;
}
// 删除
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
// 与下层元素交换
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
// 自底向上,不断比较、交换
private void siftUp(int k, E x) {
if (comparator != null) // 用自定义比较器
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x); // 用自然序比较器
}
// 在数组k的位置插入x
// 把x赋值给key
// 不断比较key和k的父节点e(queue[(k-1)/2])的关系
// 1、若key<e,则queue[k]=e,k回溯至e
// 2、若key>=e捉着k已经到达根节点,则结束循环
// queue[k]=e
// 时间复杂度O(logn)
private void siftUpComparable(int k, E x) {
// k数组大小,x添加元素
Comparable<? super E> key = (Comparable<? super E>) x; // 把x赋值给key
while (k > 0) {
// 当k不是根节点
int parent = (k - 1) >>> 1; // 取出k父节点parent下标
Object e = queue[parent]; // 取出parent值
if (key.compareTo((E) e) >= 0) // 比较key>=父节点e。结束循环
break;
queue[k] = e; // 赋值操作
k = parent; // k指针回溯操作
}
queue[k] = key;
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
// 自顶向下
// 时间复杂度O(logn)
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x; // x赋值给key
int half = size >>> 1; // 当不是叶子节点时执行循环
while (k < half) {
int child = (k << 1) + 1; // 左孩子
Object c = queue[child]; // 左孩子的值
int right = child + 1; // 右孩子
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) // 左孩子大于右孩子的情况
c = queue[child = right]; // right赋值给child,c的值变成右孩子的值
if (key.compareTo((E) c) <= 0) // 比较key和c的大小
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
// 构建最大堆
// 把数组看一个未调整完毕的堆
// 从第一个非叶子节点开始,从下往上、从右往左不断调用siftDown方法
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
public Comparator<? super E> comparator() {
return comparator;
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
s.defaultWriteObject();
// Write out array length, for compatibility with 1.5 version
s.writeInt(Math.max(2, size + 1));
// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
public final Spliterator<E> spliterator() {
return new PriorityQueueSpliterator<E>(this, 0, -1, 0);
}
static final class PriorityQueueSpliterator<E> implements Spliterator<E> {
/*
* This is very similar to ArrayList Spliterator, except for
* extra null checks.
*/
private final PriorityQueue<E> pq;
private int index; // current index, modified on advance/split
private int fence; // -1 until first use
private int expectedModCount; // initialized when fence set
/** Creates new spliterator covering the given range */
PriorityQueueSpliterator(PriorityQueue<E> pq, int origin, int fence,
int expectedModCount) {
this.pq = pq;
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
private int getFence() {
// initialize fence to size on first use
int hi;
if ((hi = fence) < 0) {
expectedModCount = pq.modCount;
hi = fence = pq.size;
}
return hi;
}
public PriorityQueueSpliterator<E> trySplit() {
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
return (lo >= mid) ? null :
new PriorityQueueSpliterator<E>(pq, lo, index = mid,
expectedModCount);
}
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
PriorityQueue<E> q; Object[] a;
if (action == null)
throw new NullPointerException();
if ((q = pq) != null && (a = q.queue) != null) {
if ((hi = fence) < 0) {
mc = q.modCount;
hi = q.size;
}
else
mc = expectedModCount;
if ((i = index) >= 0 && (index = hi) <= a.length) {
for (E e;; ++i) {
if (i < hi) {
if ((e = (E) a[i]) == null) // must be CME
break;
action.accept(e);
}
else if (q.modCount != mc)
break;
else
return;
}
}
}
throw new ConcurrentModificationException();
}
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null)
throw new NullPointerException();
int hi = getFence(), lo = index;
if (lo >= 0 && lo < hi) {
index = lo + 1;
@SuppressWarnings("unchecked") E e = (E)pq.queue[lo];
if (e == null)
throw new ConcurrentModificationException();
action.accept(e);
if (pq.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
public long estimateSize() {
return (long) (getFence() - index);
}
public int characteristics() {
return Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL;
}
}
}
删除排序数组中的重复项
(Facebook、字节跳动、微软在半年内面试中考过)
/*
题目:在不创建新数组的条件下,在原数组中删除重复出现的数字。
PS:数组是引用传递的,传递的是数组的头节点。对数组的修改会对调用者产生影响。
!只修改前几个数就可以了
方法一:快慢指针。题目中的数组是排序过了的,不需要单独排序。如果没排序过就Arrays.sort()
left慢指针,right快指针。
left左边是处理过的,right右边是未处理过的。
由right遍历一遍数组。left记录下一个没有重复的数放置的位置。
时间复杂度:O(n)
空间复杂度:O(1)
*/
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
// 快慢指针。left慢指针。right快指针。
// 0到left是非重复元素。right到length-1是待检测元素
int left = 0;
for (int right = 1; right < nums.length; right++) {
// 指针不相等时,将右指针的值放入左指针+1的位置
if (nums[left] != nums[right]) {
nums[++left] = nums[right];
}
}
return left + 1;
}
}
旋转数组
(微软、亚马逊、PayPal 在半年内面试中考过)
/*
题目:将数组中的整体元素向右移动k个位置。
方法一:暴力。
i -> 0, k-1
j -> 0, length -1
移动k次。没个数字都移动一遍。交换位置。
时间复杂度O(n*k)
空间复杂度O(1) 不需要额外空间
方法二:建立新数组。
先将原数组的数据拷贝到新数组中。
再将新数组中的数据拷贝到原数组中。
时间复杂度O(n*2)
空间复杂度O(n)
方法三:将数字做反转。有点像是公式。!这个解法最精妙。
先将整个数组反转。
然后将0,k-1的数字反转
再将k,len - 1的数字反转。
就是所要的结果。
时间复杂度O(n*2)
空间复杂度O(1)
方法四:直接将数字放到最后的结果位置,相应间隔的都换位置。然后下一个,同样相应间隔的都换位置。数量达到数组个数退出。
时间复杂度:O(n)
空间复杂度:O(1)
*/
/*
public class Solution {
public void rotate(int[] nums, int k) {
k = k % nums.length; // 对k取模,重复的圈数不需要计算
int count = 0; // 计数位,达到数组个数就完成了
for (int start = 0; count < nums.length; start++) {
int current = start; // 当前位置
int prev = nums[start]; // 出发值
do {
int next = (current + k) % nums.length; // 目标位置
// 交换
int temp = nums[next]; // 目标位置的值
nums[next] = prev; // 目标位置的值改为出发值
prev = temp; // 出发值改为目标位置的值
current = next; // 当前索引改为目标位置的索引
count++; // 计数+1
} while (start != current); // 相对应的间隔位的数字都会移动到目标位置
}
}
}
*/
public class Solution {
public void rotate(int[] nums, int k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
}
public void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}
合并两个有序链表
(亚马逊、字节跳动在半年内面试常考)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
题目:合并两个升序链表(已经升序处理过了),数字从小到大
解法一:递归
子问题与原问题有相同的结构
时间复杂度: O(n+m)
空间复杂度: O(n+m)
解法二:暴力迭代:比较大小,然后改指向
时间复杂度:O(n+m)
空间复杂度:O(1)
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2; // 链表1空,返回链表2
else if (l2 == null) return l1; // 链表2空,返回链表1
else if (l1.val < l2.val) {
// 链表1的值小
l1.next = mergeTwoLists(l1.next, l2); // 链表1指向 (链表1的下一个值与链表2合并)的结果
return l1; // 返回链表1
}
else {
// 链表2的值小
l2.next = mergeTwoLists(l1, l2.next); // 链表2指向 (链表1与链表2的下一个值合并)的结果
return l2; // 返回链表2
}
}
}
合并两个有序数组
(Facebook 在半年内面试常考)
/*
题目:将数组2放入数组1中,并且排序。
感觉这题靠API的使用。
解法一:先合并,后排序
System.arraycopy(nums2, 0, nums1, m, n);
Arrays.sort(nums1);
时间复杂度 : O( (n+m) * log(n+m) )
空间复杂度 : O(1)
解法二:双指针
时间复杂度 : O(n + m)
空间复杂度 : O(m)
拷贝数字1为新数组。
两个指针,比较数组1和数组2的大小,小的放入新数组中。
比较完,剩余的放在新数组后面。
解法三:三指针,从后往前放。
p指针指向要放置的位置。p1指向数组1比较的值。p2指向数组2比较的值。
时间复杂度 : O(n + m)
空间复杂度 : O(1)
*/
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// two get pointers for nums1 and nums2
int p1 = m - 1;
int p2 = n - 1;
// set pointer for nums1
int p = m + n - 1;
// while there are still elements to compare
while ((p1 >= 0) && (p2 >= 0))
// compare two elements from nums1 and nums2
// and add the largest one in nums1
nums1[p--] = (nums1[p1] < nums2[p2]) ? nums2[p2--] : nums1[p1--];
// add missing elements from nums2
System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
}
}
两数之和
(亚马逊、字节跳动、谷歌、Facebook、苹果、微软在半年内面试中高频常考)
/*
找出数组中两个数字满足目标值的两个数字
方法一:暴力迭代
i -> 0, len - 2
j -> i+1, len - 1
nums[i] + nums[j] == target
时间复杂度O(n^2)
空间复杂度O(n)
方法二: 利用哈希表,两次迭代
第一次,将所有数字放入哈希表中,
第二次,遍历哈希表,查找目标值-当前值的结果是否在哈希中
时间复杂度O(n * 2)
空间复杂度O(n)
方法三:利用哈希表,两次迭代
其实只需要一次,先判断目标值-当前值的结果是否在哈希中,
不存在,在把值放进去。
时间复杂度:O(n)
空间复杂度:O(n)
*/
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] {
map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}
移动零
(Facebook、亚马逊、苹果在半年内面试中考过)
/*
将数组中的0移动到最后,保持原来的非零数字的顺序。
要求不能开辟新数组。
方法一:
开辟新数组。遍历一次,一边统计0的个数,一将非0放入新数组中。第二次将0追加到新数组中。不满足题目要求。
time: O(n * 2)
space: O(n)
方法二:
开辟新数组。双指针。一个指针指向头,非0数字放入,一个指针指向尾,0放入。不满足题目要求。
time: O(n)
space: O(n)
方法三:
一个指针。记录非0元素放置的位置target。遍历一次,非0就放入target的位置。
time: O(n)
space: O(1)
*/
public class Solution {
public void moveZeroes(int[] nums) {
int target = 0;
for(int i = 0; i < nums.length; i++)
// 遍历到的数字非0,放到目标位置。
if(nums[i] != 0) {
int tmp = nums[target];
nums[target] = nums[i];
nums[i] = tmp;
target++;
}
}
}
加一
(谷歌、字节跳动、Facebook 在半年内面试中考过)
/*
一个数字组成的数组。转成数字,然后+1。再转成数组
方法一:从后往前遍历。
目标值不是9,直接+1,结束。
目标值是9,当前位置值变为0,下个数+1。
最后还是没结束,说明碰到了9999,需要开辟新数组,头为1。
time: O(n)
space: O(n+1)
*/
class Solution {
public int[] plusOne(int[] digits) {
int len = digits.length;
// 从后往前遍历
for (int i = len-1; i >= 0; i--) {
// 数字不是9,直接自增,返回结果。
if (digits[i] != 9) {
digits[i]++;
return digits;
}
// 是9,当前值变为0。继续遍历,把下个值+1。
digits[i] = 0;
}
// 没结束,说明碰到了9999这种情况,开辟新数组,数组头写成1。
int[] newNumber = new int [len+1];
newNumber[0] = 1;
return newNumber;
}
}
中等
设计循环双端队列
(Facebook 在 1 年内面试中考过)
/*
构造双端队列。用链表,两个指针。两个头尾哨兵
*/
class DoubleListNode {
DoubleListNode pre; // 前指针
DoubleListNode next; // 后指针
int val; // 值
public DoubleListNode(int val) {
this.val = val;
}
}
class MyCircularDeque {
int size; // 实际个数
int k; // 开辟大小
DoubleListNode head; // 虚拟头节点哨兵
DoubleListNode tail; // 虚拟尾节点哨兵
/**
* 初始化,大小为k。
*/
public MyCircularDeque(int k) {
head = new DoubleListNode(-1); // 头节点,默认值-1
tail = new DoubleListNode(-1); // 为节点,默认值-1
head.pre = tail; // 头节点前指针指向尾
tail.next = head; // 尾节点后指针指向头
this.k = k;
this.size = 0;
}
/**
* 将一个元素添加到双端队列头部。 如果操作成功返回 true。
*/
public boolean insertFront(int value) {
if (isFull()) return false; // 容量不够,返回false
DoubleListNode node = new DoubleListNode(value); // 创造新节点
// 新节点后指针指向头。新节点前指针指向头节点的前一个。头节点的前一个后指针指向当前节点。头节点的前指针指向新节点。
node.next = head;
node.pre = head.pre;
head.pre.next = node;
head.pre = node;
size++;
return true;
}
/**
* 将一个元素添加到双端队列尾部。如果操作成功返回 true。
*/
public boolean insertLast(int value) {
if (isFull()) return false;
DoubleListNode node = new DoubleListNode(value);
node.next = tail.next;
tail.next.pre = node;
tail.next = node;
node.pre = tail;
size++;
return true;
}
/**
* 从双端队列头部删除一个元素。 如果操作成功返回 true。
*/
public boolean deleteFront() {
if (isEmpty()) return false; // 元素空了,返回false
head.pre.pre.next = head; // 头节点的前前一个后指针指向头节点
head.pre = head.pre.pre; // 头节点的前指针指向头节点的前前一个
size--;
return true;
}
/**
* 从双端队列尾部删除一个元素。如果操作成功返回 true。
*/
public boolean deleteLast() {
if (isEmpty()) return false;
tail.next.next.pre = tail;
tail.next = tail.next.next;
size--;
return true;
}
/**
* 从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
*/
public int getFront() {
return head.pre.val;
}
/**
* 获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
*/
public int getRear() {
return tail.next.val;
}
/**
* 检查双端队列是否为空。
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 检查双端队列是否满了。
*/
public boolean isFull() {
return size == k;
}
}
困难
接雨水
(亚马逊、字节跳动、高盛集团、Facebook 在半年内面试常考)
/*
一个数组表示柱子。问能接到多少水。
方法一:暴力
方法二:DP
方法三:栈
后面的柱子比前面的柱子低,是无法积水的。
用栈存储柱子下标。
方法四:双指针
*/
public class Solution {
public int trap(int[] height) {
// 存索引下标
Stack<Integer> stack = new Stack<>();
int current = 0, sum = 0;
while (current < height.length) {
// 如果栈不空并且当前柱子高度大于栈顶柱子的高度,就进入循环
while (!stack.empty() && height[current] > height[stack.peek()]) {
int top = stack.pop(); // 取出栈顶元素的索引
if (stack.empty()) break; // 栈空就结束
int distance = current - stack.peek() - 1; // 两个柱子之间的距离
int min = Math.min(height[current], height[stack.peek()] - height[top]); // 取当前柱子和前两个柱子高度差
sum += distance * min;
}
stack.push(current++); // 当前柱子索引入栈
}
return sum;
}
}