数据结构是数据存储的结构,算法是在特定的数据结构上对数据进行计算的过程,软件的核心功能是计算数据,所以就有了程序=数据结构+算法的说法。
对于数据结构来说,可以分为线性数据结构和非线性数据结构,线性数据结构中包含两种基础的线性数据结构数组和链表,同时,还包含两种高级线性数据结构栈和队列,栈和队列可以用数组或链表来实现;非线性数据结构中比较基础且常见的就是跳表、哈希表、树和图三种,树又可以分为二叉搜索树、AVL树、红黑树、堆、线段树、字典树、B树/B+树等。
学习数据结构可以从三个方面入手,第一是数据结构是实现方式,第二是这种数据结构的特点及相关操作的时间复杂度,因为特性和时间复杂度决定了应用场景,第三是可以解决什么问题,对于这方面可以从算法的角度和实际应用的角度来看。
对于算法来说,比较常见和常考察的右排序、搜索、贪心、回溯、动态规划、最短路径、分治等。学习算法没有什么捷径,只能是多刷题、多思考多理解,另外,一些基础的算法在一些框架和编程语言中被广泛的使用,可以结合实际来了解一种算法在工程应用领域的实践,这样有助于更好的理解算法。
寻求反馈对于提高学习效率非常有帮助,比如可以找朋友一起学,然后讨论问题,还可以把碰到的问题找大牛请求,或者通过写博客来被动得到读者的反馈等,总之,在讨论中学习是最好的学习方式,无论讨论的形式是怎么样的。
复杂度时衡量一个算法好坏的标准,一般分为时间复杂度和空间复杂度两种,时间复杂度主要用来衡量算法运行时间与数据量之间的关系,空间复杂度指的是算法在运行过程中,需要额外开辟的空间的大小,上图是常见的复杂度曲线图。
复杂度是指一个理论趋势,实际场景中并不一定说O(n)复杂度的算法就一定比O(logn)的算法要快。
线性数据结构中最主要的就是数组、链表、栈和队列,数组和链表是最基础的线性数据结构,他们是一部分高级数据结构的基础。
数组可以理解为是一块连续且有序的内存,用来存储相同类型的数据,链表与数组相反,它是通过指针链的方式将不连续的内存空间串联起来,理论上没有空间大小的限制,相关操作的时间复杂度对比如下:
数组 | 链表 | |
---|---|---|
添加 | O(n) | O(1) |
删除 | O(n) | O(1) |
查找 | O(1) | O(n) |
可以看出,数组支持基于下标的快速随机查找,但添加和删除操作由于要移动数据,所以效率比较低,而链表由于无法计算偏移量,所以查找的效率比较低,但添加和删除操作由于只需要调整指针就能够实现,所以效率比较高,这就区分了两种线性表的使用场景。在实际使用高级语言编程时,我们一般使用的是动态线性表,比如Java中的ArrayList和LinkedList,他们分别是基于数组和链表实现的,下面基于数组和链表实现一个自己的List。基于数组实现的List的核心在于动态扩容,基于链表实现的List的核心在于指针调整。
/**
* @author Echo
* @date 2020/5/30 1:22 下午
* 基于数组实现的动态链表
*/
public class MyArrayList {
private T[] data;
private int size;
public MyArrayList() {
this(10);
}
public MyArrayList(int capacity) {
this.data = (T[])new Object[capacity];
this.size = 0;
}
//在末尾添加
public void add(T e) {
if(size == data.length) {
resize(size + 1);
}
data[size ++] = e;
}
//在指定位置添加
public void add(int index, T e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("illegal index.");
}
if(size == data.length) {
resize(size + 1);
}
for (int i = size - 1;i >= index;i --) {
data[i + 1] = data[i];
}
data[index] = e;
size ++ ;
}
public void addFirst(T e){ add(0, e); }
public void addLast(T e){
//如果整个list为空,addLast和addFirst一样 添加到第一个位置
if (size == 0) {
add(0, e);
}
add(size - 1, e);
}
private void resize(int newCapacity) {
int newLength = Math.max(2 * size, newCapacity);
T[] newData = (T[])new Object[newLength];
System.arraycopy(data, 0, newData, 0, size);
data = newData;
}
//查找
public T get(int index) {
if (isEmpty()) {
return null;
}
if (index < 0 || index >= size) {
throw new IllegalArgumentException("illegal index.");
}
return data[index];
}
public T getFirst() {
return get(0);
}
public T getLast() {
if (size == 0) {
return get(0);
}
return get(size - 1);
}
//删除指定位置上的值
public T remove(int index) {
if (isEmpty() || index < 0 || index >= size) {
throw new IllegalArgumentException();
}
T del = data[index];
for (int i = index;i < size - 1;i ++) {
data[i] = data[i + 1];
}
data[size - 1] = null; // help GC
size -- ;
return del;
}
public T removeFirst() {
return remove(0);
}
public T removeLast(){
return remove(size - 1);
}
//获取一个指定元素的索引值
public int getIndex(T e) {
for (int i = 0;i < size;i ++) {
if(data[i].equals(e)) {
return i;
}
}
return -1;
}
public int getSize() {
return size;
}
public int getCapacity() {
return data.length;
}
public boolean isEmpty() {
return size == 0;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("MyArrayList [ ");
for (int i = 0;i < size;i ++) {
stringBuilder.append(data[i] + " ");
}
stringBuilder.append("]");
return stringBuilder.toString();
}
public static void main(String[] args) {
MyArrayList arrayList = new MyArrayList<>(2);
arrayList.add(1);
arrayList.add(2);
System.out.println(arrayList);
arrayList.add(3);
arrayList.add(4);
System.out.println(arrayList);
System.out.println(arrayList.getCapacity());
arrayList.add(2, 10);
System.out.println(arrayList);
System.out.println(arrayList.getCapacity());
arrayList.remove(2);
System.out.println(arrayList);
}
}
/**
* @author Echo
* @date 2020/5/30 1:22 下午
* 基于单链表的动态链表
*/
public class MyLinkedList {
private Node dummyHead;
private int size;
public MyLinkedList() {
dummyHead = new Node(null);
size = 0;
}
//朝招指定位置上的值
public E get(int index) {
if (isEmpty() || index < 0 || index >= size) {
throw new IllegalArgumentException();
}
//找到index位置的node
Node cur = dummyHead;
for (int i = 0;i <= index;i ++) {
cur = cur.next;
}
return cur.value;
}
public E getFirst() { return get(0); }
public E getLast() { return get(size - 1); }
//在指定位置添加数据
public void add(int index, E e){
if (index < 0 || index > size) {
throw new IllegalArgumentException();
}
//找到index的前一个位置
Node cur = dummyHead;
for (int i = 0;i < index;i ++) {
cur = cur.next;
}
cur.next = new Node(e, cur.next);
this.size ++ ;
}
public void addFirst(E e) {
add(0, e);
}
public void addLast(E e){
if (size == 0) {
add(0, e);
return;
}
add(size, e);
return;
}
//删除指定位置上的数据
public E remove(int index){
if (isEmpty() || index < 0 || index >= size) {
throw new IllegalArgumentException();
}
//找到待删除位置的前一个位置
Node cur = dummyHead;
for(int i = 0;i < index;i ++) {
cur = cur.next;
}
Node delNode = cur.next;
cur.next = delNode.next;
delNode.next = null;
return delNode.value;
}
public E removeFirst() { return remove(0);}
public E removeLast(){
return remove(size - 1);
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
//单链表
private class Node {
E value;
Node next;
public Node(E value) {
this(value, null);
}
public Node(E value, Node next) {
this.value = value;
this.next = next;
}
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("MyLinkedList [ ");
Node cur = dummyHead.next;
while (cur != null) {
stringBuilder.append(cur.value + " ");
cur = cur.next;
}
stringBuilder.append(" ]");
return stringBuilder.toString();
}
public static void main(String[] args) {
MyLinkedList linkedList = new MyLinkedList<>();
linkedList.addFirst(0);
linkedList.addFirst(1);
System.out.println(linkedList);
linkedList.addLast(2);
System.out.println(linkedList);
linkedList.add(1, 4);
System.out.println(linkedList);
linkedList.remove(2);
System.out.println(linkedList);
}
}
栈的特点是后进先出,可以理解为是线性表的一种子集,栈可以通过链表和数组来实现。
/**
* @author Echo
* @date 2020/5/30 2:45 下午
*/
public class ArrayStack {
private MyArrayList array;
public ArrayStack(int capacity) {
array = new MyArrayList<>(capacity);
}
public void push(E e){ array.addFirst(e); }
public E peek() { return array.getFirst(); }
public E pop() { return array.removeFirst(); }
public int getSize() { return array.getSize(); }
public boolean isEmpty(){ return array.isEmpty(); }
}
/**
* @author Echo
* @date 2020/5/30 2:50 下午
*/
public class LinkedStack {
private MyLinkedList linkedList;
public LinkedStack() {
linkedList = new MyLinkedList<>();
}
public void push(E e) { linkedList.addFirst(e); }
public E peek() { return linkedList.getFirst(); }
public E pop() { return linkedList.removeFirst(); }
public boolean isEmpty(){ return linkedList.isEmpty(); }
public int getSize() { return linkedList.getSize(); }
}
队列和栈一样,是一种特殊的线性表,它的特点是先进先出,队列可以基于数组实现也可以基于链表实现,基于链表的实现比较简单,而基于数组实现时,为了避免在入队和出队时频繁移动数据,可以通过通过循环数组来实现,这样,虽然队列容量不能像链表那样不受限制,但可以保证入队和出队的时间复杂度都是O(1),下面是一个基于循环数组实现的简易队列
/**
* @author Echo
* @date 2020/5/30 2:55 下午
* 基于数组实现的循环数组
*
* front == tail 为空
* (tail + 1) % length 为满
*/
public class ArrayLoopQueue {
private int[] data;
private int front, tail;
private int size;
public ArrayLoopQueue(int capacity) {
this.data = new int[capacity];
this.size = 0;
this.front = this.tail = 0;
}
public void enqueue(int e) {
if ((tail + 1) % data.length == front) {
resize(data.length * 2);
}
data[(++ tail) % data.length] = e;
tail = tail % data.length;
this.size ++ ;
}
public int dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException();
}
int ret = data[front];
front = (front + 1) % data.length;
size -- ;
return ret;
}
private void resize(int newCapacity) {
int[] newData = new int[newCapacity];
for (int i = 0;i < size;i ++) {
newData[i] = data[(i + front) % data.length];
}
this.data = newData;
front = 0;
tail = size - 1;
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
非线性数据结构的维度要比较线性数据结构高,一般是二维结构,比如矩阵或树,升维一般是解决复杂问题的有效办法,所以非线性数据结构总体上要比线性数据结构复杂得多。
https://www.geeksforgeeks.org/skip-list/
跳表是一种非线性数据结构,它的主要作用提高针对有序链表的操作效率问题,基本的思路就是在链表的基础上进行升维和以空间换时间,链表操作的时间复杂度为O(n),为了提高效率,跳表在原链表的基础上增加了一层或多层区间索引,这样就能够通过区间索引快速定位要找的数据,把O(n)的时间复杂度降到了O(logn),查找过程有点类似于二分搜索树或者二分查找。
二分搜索树是一种特殊的二叉树,左子节点的值<当前节点的值<右子节点的值,理想情况下,基于二分搜索树的操作的复杂度能够达到O(Logn),但二分搜索树却很少被用到实际的工程当中,更多的是在学习数据结构时,作为树的入门知识,这主要是因为二分搜索树无法保证平衡性,在极端条件下甚至会退化成链表, 但这并不是说二分搜索树没有意义。
/**
* @author Echo
* @date 2020/5/30 10:11 下午
* 实现二分搜索树
*/
public class BST2> {
private Node root;
private int size;
public BST2() {
this.root = null;
this.size = 0;
}
//添加节点
public void add(E e) {
root = add(e, root);
}
//在node为根节点的树中添加e
private Node add(E e, Node node) {
if (node == null) {
size ++ ;
return new Node(e);
}
if (e.compareTo(node.value) < 0) {
node.left = add(e, node.left);
}else if (e.compareTo(node.value) > 0) {
node.right = add(e, node.right);
}else {
node.value = e;
}
return node;
}
//判断是否包含指定的元素
public boolean contains(E e) {
return contains(e, root);
}
private boolean contains(E e, Node node) {
if (node == null) {
return false;
}
if (e.compareTo(node.value) < 0) {
return contains(e, node.left);
}else if (e.compareTo(node.value) > 0){
return contains(e, node.right);
}
return true;
}
//找到最大值
public E min() {
if (isEmpty()) {
return null;
}
return min(root).value;
}
private Node min(Node node) {
if (node.left == null) {
return node;
}
return min(node.left);
}
//找到最小值
public E max() {
if (isEmpty()) {
return null;
}
return max(root).value;
}
private Node max(Node node) {
if (node.right == null) {
return node;
}
return max(node.right);
}
//直到给定值对应的Node节点
private Node getNode(E e) {
return getNode(e, root);
}
private Node getNode(E e, Node node) {
if (node == null) {
return null;
}
if (e.compareTo(node.value) < 0) {
return getNode(e, node.left);
}else if (e.compareTo(node.value) > 0) {
return getNode(e, node.right);
}
return node;
}
//删除最小值
public E removeMin() {
E min = min();
root = removeMin(root);
return min;
}
private Node removeMin(Node node) {
if (node.left == null) {
Node right = node.right;
node.right = null;
size -- ;
return right;
}
node.left = removeMin(node.left);
return node;
}
//删除最大值
public E removeMax() {
E max = max();
removeMax(root);
return max;
}
private Node removeMax(Node node) {
if (node.right == null) {
Node left = node.left;
node.left = null;
this.size -- ;
return left;
}
node.right = removeMax(node.right);
return node;
}
//删除任意给定的值
public void remove(E e) {
Node node = getNode(e);
if (node == null) {
throw new IllegalArgumentException();
}
return;
}
//从以node为根的树中删除e所在的节点
private Node remove(E e, Node node) {
if (e.compareTo(node.value) < 0) {
node.left = remove(e, node.left);
return node;
}else if (e.compareTo(node.value) > 0) {
node.right = remove(e, node.right);
return node;
} else {
//找到了, 没有左子节点
if (node.left == null) {
Node right = node.right;
node.right = null;
this.size -- ;
return right;
}
if (node.right == null) {
Node left = node.left;
node.left = null;
this.size -- ;
return left;
}
//即有左子节点右有右子节点
Node sucessor = max(node.right);
sucessor.right = removeMax(node.right);
sucessor.left = node.left;
node.left = node.right = null;
return sucessor;
}
}
//前序遍历 递归实现
public void preOrder(){
preOrder(root);
}
private void preOrder(Node node) {
if (node == null) {
return;
}
System.out.println(node.value);
preOrder(node.left);
preOrder(node.right);
}
//前序遍历 非递归实现
public void preOrder2() {
Stack stack = new Stack<>();
stack.add(root);
while (!stack.isEmpty()) {
Node top = stack.pop();
System.out.println(top.value);
if (top.left == null) {
stack.add(top.left);
}
if(top.right == null) {
stack.add(top.right);
}
}
}
//层序遍历
public void levelOrder() {
Queue queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
Node node = queue.remove();
System.out.println(node.value);
if (node.left != null) {
queue.add(node.left);
}
if(node.right != null) {
queue.add(node.right);
}
}
}
public boolean isEmpty() {
return size == 0;
}
private class Node {
E value;
Node left;
Node right;
public Node(E value) {
this(value, null, null);
}
public Node(E value, Node left, Node right) {
this.value = value;
this.left = left;
this.right = right;
}
}
}
哈希有时候也被称为散列,是一种支持快速检索的数据结构,时间复杂度可以达到O(1),理论上的哈希表实际上就可以理解成一个数组,只不过,在操作这个数组中的数据的时候,要借助一个叫做哈希函数的东西来计算索引,这个哈希函数的作用就是尽量将数据随机的分布到数组(也就是哈希表)中。
但无论这个哈希函数再怎么牛逼,也不可能保证两个数据的哈希值一定不同,如果两个元素的哈希值相同,那就出现了哈希冲突,解决哈希冲突的常见方式是链地址发,就是在数组的每个位置上如果出现了哈希冲突,就构造一个拉链,把哈希值相同的元素放到这个拉链上(这就是我们熟悉的数组+链表的实现方式)。
一般在实现哈希表时,都会设置加载因子,加载因子表示的是哈希表的负载情况,当超过给定的阈值时,就会对哈希表进行扩容,比如如果设置加载因子为1,就表示只有当数量总量达到哈希表的大小时再出发扩容,而如果把加载因子设置成0.5,就表示数据量达到整个哈希表容量的一半时,就会触发扩容,加载因子过大,哈希冲突的几率就会增加,效率就会降低,反之虽然保证了性能,但是空间利用率会下降,Java中的hash map把加载因子设置为0.75,这也是在实践和空间上的一种平衡。
说到哈希,散列算法,散列算法也可以叫做哈希算法,哈希算法在中间件和分布式系统中都有广泛的应用,比如数据加密(MD5+盐值)、唯一标识、数据完整性校验、负载均衡、数据分片(比如分库分表等)、分布式存储(HDFS)、区块链等。
比较常见的哈希算法有MD5、SHA、AES。
了解HashMap可以从两个方面入手,首先是数据结构,其次是扩容机制。在jdk1.8中,HashMap采用哈希表+单链表+红黑树的方式实现,通过单链表来解决哈希冲突的问题,为了解决哈希冲突严重导致链表过长,最终影响HashMap的效率的问题,当单链表的长度大于8时,单链表就会被转换成一棵红黑树,这样就某个哈希槽位上的链表来说,时间复杂度就从O(n)降低到了O(logn),当链表长度小于6时,红黑树又会被转换成链表,这是因为,在小数据量下维护红黑树的开销会大于它带来的性能提升。
HashMap的默认加载因子是0.75,默认容量是16,对于扩容,在jdk1.8前是通过重新计算每一个元素的hash code然后与扩容后的哈希表的长度取模完成的,并且对于某个哈希槽位上的单链表来说,是反向插入,这就造成了大家常说的“HashMap在并发环境下会出现死循环”的问题, 而在jdk1.8及以后的版本中,对整个扩容逻辑进行了优化,首先,它会保证哈希表的长度为2的整数次幂,然后再扩容重新分配元素到新的哈希表时,通过与位运算替代原来的取模,这样就可以保证一个元素在扩容后的哈希表中的位置要么与原来的位置一样,要么在原位置的2倍的位置,这样就可以大大提高扩容时重新分配元素到新的哈希表的效率。
另外,HashMap把加载因子设定为0.75是一种空间和时间上的平衡。
堆是一种很重要的数据结构,最熟悉的场景就是堆排序。堆是一种特殊的二叉树,更准确的说,堆是一种特殊的完全二叉树,它的主要特性就是父亲节点大于子节点,重点在于它没有规定那个子节点比父节点大(如果规定了那就变成二叉搜索树了),堆的完全二叉树的特定保证了它的平衡性,实现了基于堆的操作的时间复杂度都是O(logn),处理在排序中,堆还被用在实现优先级队列中,因为在一个堆中,堆定元素要么是最大的,要么是最小的。
在实现上,堆可以基于类似于二分搜索树的指针引用的方式来实现,也可以基于数组来实现,由于堆是一个完全二叉树,所以用数组表示起来会很方便。
public class MaxHeap> {
private LIst data;
public MaxHeap() {
this(10);
}
public MaxHeap(int capacity) {
this.data = new ArrayList<>(capacity);
}
public MaxHeap(E[] es){
this.data = new ArrayList<>(Arrays.asList(es));
//从最后一个元素的父节点开始,执行下沉操作
for (int i = getParent(getSize() - 1); i >= 0;i --) {
siftDown(i, es.length);
}
}
/**
添加
*/
public void add(E e) {
//在末尾添加元素
this.data.add(size(), e);
//执行上浮操作
siftUp();
}
private void siftUp(int index) {
//如果父节点比当前节点小,就交换位置
while(index > 0 && data.get(getParent(index)).compareTo(data.get(index)) < 0) {
E temp = data.get(getParent(index));
data.set(getParent(index), data.get(index));
data.set(index, temp);
}
}
//获取最大元素,也就是堆顶元素
public E getMax(){
if (isEmpty()) {
throw new IllegalArgumentException("heap is empty");
}
return data.get(0);
}
/**
移除堆定元素
*将最后一个元素放到堆定,然后执行下沉操作
*/
public E extractMax() {
E max = data.get(0);
//把堆定元素和最后一个元素调换位置
E temp = data.get(getSize() - 1);
data.set(0, data.get(getSize() - 1));
data.set(getSize() - 1, temp);
//移除最后一个元素,即原堆顶元素
data.remove(getSize() - 1);
siftDown(0, getSize() -1);
return max;
}
private void shiftDown(int index, int size) {
//终止条件是index节点的左子树索引>size,此时说明已经遍历到最后一个元素了
while(getLeftChild(index) < size) {
int idx = getLeftChild(index);
if(index + 1 < size &&
data.get(index + 1).compareTo(data.get(index) > 0)) {
//如果右子节点比左子节点大,更新idx为右子节点
idx = idx+ 1;
}
//如果父节点已经比子节点大,就说明已经siftDown完成了
if (data.get(index).compareTo(data.get(idx)) > 0) {
break;
}
else {
//交换位置
E temp = data.get(index);
data.set(index, data.get(idx));
data.set(idx, temp);
//向下推进
index = idx;
}
}
}
public int getParent(int index) {
return (index - 1) / 2;
}
public int getLeftChild(int index){
return index * 2 + 1;
}
public getRightChild(int index) {
return index * 2 + 2;
}
public int getSize() {
return this.data.size();
}
public boolean isEmpty() {
return this.data.isEmpty();
}
}
根据猎头的 ID 快速查找、删除、更新这个猎头的积分信息;
查找积分在某个区间的猎头 ID 列表;
查找按照积分从小到大排名在第 x 位到第 y 位之间的猎头 ID 列表
以猎头id构造一个散列表,然后用积分构造一个跳表,单列表支持O(1)的按id查询,跳表支持按积分区间查询。