数据结构和 Collection
数据结构基本上来说都是用来存储数据的,可以看作是一个容器,里面用来装数据。根据不同需求,需要不同种类的容器。把数据看作是啤酒的话,可以选择瓶子或者易拉罐做容器。
很多语言并没有提供容器的实现(没有瓶子也没有易拉罐,想要用,自己造),比方说C,比方说JavaScript,所以国内很多教材都是通过C语言来学习数据结构,只因为可以更强的理解数据结构。
Java提供了一个强大的包: Collection,几乎涵盖了所有的数据结构,意思就是,我们可以直接挑一个容器来存储数据,不用自己去写这个容器。(玻璃瓶易拉罐罐子木桶…都准备好了)
但是这并不意味着用Java就不需要去了解这些结构,因为,很多面试官就喜欢问,这种情况下,你要用什么来存储数据啊?
面对这种问题,需要考虑以下几个方面:
- 数据类型是什么
- 存储的数据是需要排序的么
- 存储的数据允许重复么
- 这些数据会经常增删么
- 这些数据会被频繁的访问么
上面这些问题的答案整明白之后,就需要了解每种数据结构的特点,然后去选一个来回答面试官。如果是java面试,需要把对应的类和方法也说出来。
其实,青岛人的啤酒是用塑料袋装的。。。
1. 线性结构
数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。
如(a1,a2,a3,…,an),a1为第一个元素,an为最后一个元素,此集合即为一个线性结构的集合。
线性结构特点:
- 集合中必存在唯一的一个"第一个元素";
- 集合中必存在唯一的一个"最后的元素";
- 除最后元素之外,其它数据元素均有唯一的"后继";
- 除第一元素之外,其它数据元素均有唯一的"前驱"。
1.1 Array(数组)
图片来源https://www.geeksforgeeks.org/arrays-in-java/
- 一段连续的存储空间。逻辑上连续,不是物理空间上连续。
- array可以用来存一切数据。可以用来存一个cunstom的class
Student[] arr = new Student[7]; //student is a user-defined class
- 也可以用来存另一个array。那就是二维数组。同理,三维数组。。。N维数组
注意: 多维数组不属于线性结构int[][] intArray = new int[10][20]; //a 2D array or matrix int[][][] intArray = new int[10][20][10]; //a 3D array
图片来源https://www.geeksforgeeks.org/arrays-in-java/
以下几点是Java中array的特点!跟array这个概念没关系!
- java中用之前就要定义长度,定义长度之后就不能改了,如果尝试改长度会有compile error。 java中如果尝试访问超过长度的数组叫做数组越界,是runtime exception
- java中定义数组的方法:两种定义方法都是可以的
int intArray[] = new int[20]; int[] intArray = new int[20];
- 要在申请空间的时候就定义大小!后面那个中括号里要声明长度。或者用下面方式定义
int[] intArray = new int[]{ 1,2,3,4,5 } int[] intArray = { 1,2,3,4,5 }
- 可以用clone来复制一个array。注意,clone这个方法只能复制一层array。clone一维数组是全部copy。二维数组是shalow copy. 详细讲解可以看 https://www.geeksforgeeks.org/arrays-in-java/
int intArray[] = {1,2,3}; int cloneArray[] = intArray.clone();
1.2 ArrayList(动态数组)
ArrayList 是java里提供的可以自由扩张的array。像JavaScript就不需要这种东西,因为javascript对array的操作毫无节操可言。(JavaScript 踩过的坑)。
在算法层面上来说,它还是Array。
- ArrayList是属于Collection这个包的。
- 先来个栗子尝尝鲜:
ArrayList<Integer> arrL = new ArrayList<Integer>(2); // 添加数据 arrL.add(1); arrL.add(2); arrL.add(3); System.out.println(arrL.get(1)); // 2 arrL.remove(0);// index System.out.println(arrL.get(1)); // 3 //其中Type是说明要往arrL里面存的数据类型。
上面例子中需要注意
- 存的是数字,但是定义的时候声明要存的是Integer,因为。ArrayList只可以存 Object。如果想存基本数据类型,就要存他们的Wrapper。
- 添加数据的时候用add(),一个个填,填进去的东西会一个个往后放
- 声明的时候说是2个,但是后来加了三个数字,这是可以的!因为它自己可以扩张!
- 删除的时候remove(),里面的参数是要删除的节点的index。
- 访问数据的时候用get(),里面的参数还是index。
- 前面的数据删除之后,后面的数据会往前移动。
- 既然是一个可以总有扩张的array,就要考虑这几个问题既然是一个可以总有扩张的array,就要考虑这几个问题
i. 一开始的长度是多少
答:如果new的时候声明长度了,初始时的长度就是参数里的长度。如果没有任何参数,则是默认长度,10。
ii. 什么时候扩张
答:arraylist满了的时候。
iii. 每次扩张多少
答50%
1.2.2 Array和ArrayList的区别
- Array是Java提供的一个基本的用来存储数据的东西。而ArrayList是Collection frame里提供的东西,如果要用ArrayList必须在文件头部加上
import java.util.ArrayList;
- Array里所有的添加访问删除都靠一个[]就可以搞定,但是ArrayList需要一系列的add,get,remove等方法来完成这些
- Array是要一开始就说明大小的,且后期不能改的。ArrayList一开始可以说明大小,也可以不说明,就算是说明了大小了后期可以随时扩大。
- Array可以存所有的东西,基本数据类型或者object都可以。而ArrayList只可以存Object,就算是要存数字,也要存成Integer
- 因为ArrayList不可以存基本数据类型,所以ArrayList存的都是reference,指向其他的空间。Array里存的数据是不是相邻的取决于存的是啥。所以其实理论上来讲,连续存储只能算是reference是连续的。
1.3 Vector(向量)
这玩意儿和ArrayList几乎一模一样。为啥要有呢。因为C++有。。。。
造出个和ArrayList一样的东西来跟C++攀比也不合适,所以就有了以下区别:
- ArrayList是在已经满了的时候增长50%。Vector增长100%(两倍)
- Vector提供indexOf(obj)接口,ArrayList没有。
- Vector属于线程安全的,ArrayList可以同时被多线程access。但是很少用Vector,因为线程安全开销很大。而且ArrayList也可以很简单的用下面方法做成线程安全的:
List<String> list = Collections.synchronizedList(new ArrayList<String>());
1.4 Linked List(链表)
1.4.1 Linked List 单向链表
前面三个在数据结构层面上来都是Array。是连续的存储结构。Linked List是不连续的。 在linked list里面除了有一块空间用来存储数据,还有一块儿空间要存一个reference,指向下一个数据。大概就是这个样子了。
图片来源: https://www.geeksforgeeks.org/linked-list-set-1-introduction/
- 是一个线性的存储结构,存储空间不是连续的,逻辑上和实际上都不连续。
- 最开始那个叫head,最后一个节点是null。每一个节点分成两部分,一部分是要存储的数据,另一部分是一个pointer指向下一个数据。最后连成一串儿。火车大概就是这样的,一个拉着一个,每个都掉不了链子。
- 这里有个代码例子,解释的很好 https://www.geeksforgeeks.org/linked-list-set-1-introduction/
1. 为什么要用Linked List?因为Array和ArrayList某些时候太废了啊。
- Array是固定大小的,不能随意扩张。就算是ArrayList在随意扩张的时候也是需要造一个新的大的array然后把原来的数据都挪进去。
- Array和ArrayList在中间添加数据的时候都需要把后面的数据往后挪,从中间删除数据的时候也需要把后面的数据往前挪。事件复杂度都是O(n)。
- 就算是顺序的添加数据,如果ArrayList满了需要复制整个List去一个新的,所以最坏的情况下时间复杂度也是O(n)。
针对这些问题,Linked List的时间复杂度好很多,添加的时候随时添加,不用担心size问题。如果想往中间增加或者删除一个节点,时间复杂度也是O(1),搞一搞pointer指向的节点就行了。
2. 当然相比起Array来也有缺点:
- 不能用index直接访问某个节点。像Array可以用arr[2]来访问第三个节点。ArrayList可以用get(2)来访问第三个节点。但是LinkedList没法儿这么干,如果想找到某个节点,就一溜儿的查下去,所以时间复杂度是O(n)。
- 除了正儿八经的数据占了空间,还要辟出块儿空间来存指向下一个数据的pointer。pointer也是要空间的啊!
Java已经有一个LinkedList的类了,可以直接拿来用,下面是个栗子:
import java.util.*;
public class LinkedListTest {
public static void main(String args[]) {
// Creating object of class linked list
LinkedList<String> ll = new LinkedList<String>();
// Adding elements to the linked list
ll.add("A"); // A 时间复杂度为O(1)
ll.add("B"); // A->B 时间复杂度为O(1)
ll.addLast("C"); // A->B->C 时间复杂度为O(1)
ll.addFirst("D"); // D->A->B->C 时间复杂度为O(1)
ll.add(2, "E"); //指定添加位置 D->A->E->B->C 时间复杂度为O(n)
System.out.println("Linked list : " + ll);
// Removing elements from the linked list
ll.remove("B"); //D->A->E->C 时间复杂度为O(n)
ll.remove(2); //D->A->C 时间复杂度为O(n)
ll.removeFirst(); //A->C 时间复杂度为O(1)
ll.removeLast(); //A 时间复杂度为O(1)
System.out.println("Linked list after deletion: " + ll);
// Finding elements in the linked list
boolean status = ll.contains("A"); //时间复杂度为O(n)
System.out.println("List contains the element 'A' ?" + status);
// Number of elements in the linked list
int size = ll.size();
System.out.println("Size of linked list = " + size);
// Get and set elements from linked list
Object element = ll.get(0); //时间复杂度为O(n)
System.out.println("Element returned by get() : " + element);
ll.set(0, "Y"); //可以set指定位置的值
System.out.println("Linked list after change : " + ll);
}
}
-
LinkedList的时间复杂度几乎都浪费在查找节点上了,只要找到了节点各种操作时间复杂性都是O(1)。但是查找节点,不管是通过index还是通过value,都是O(n)。
-
所以什么时候用LinkedList,什么时候用ArrayList?
数据需要经常增加删减的时候就用LinkedList。其他的基本上都用ArrayList就好。数据需要经常增加删减的时候就用LinkedList。其他的基本上都用ArrayList就好。
1.4.2 doubly linked list(双向链表)
图片来源: https://www.geeksforgeeks.org/doubly-linked-list/
每一个节点都有一个指向后面节点的指针还有个指向前面节点的指针。
1. 比起单向列表的好处:
- 删除节点更快一些。因为在单向链表中只有指向后面的指针。在删除节点的时候需要把前面一个节点的指针指向后面一个节点的指针。所以为了找到前面一个节点,有时候需要再从头走一遍。
- 往指定的节点前面加节点的时候也比较快,理由同上。
2. 比起单向列表的坏处:
- 多一个空间存储向前的指针
- 操作起来麻烦。
最后,java里没有现成的doubly linked list的类。要用的话自己写一个吧。
1.5 Queue(队列)
只需要记住四个字:先进先出!!!
记不住怎么办,买车票的时候排的那个队就叫queue。。。其工作原理和数据结构里的一模一样。。。谁先来的谁先拿票走人。。。
图片来源: https://www.studytonight.com/data-structures/queue-data-structure
- Queue 插入数据的时候插到最后面,删除数据的时候从最前面删除。先进先出。(和买票排队一样。。。)
- 可以用任何的数据结构来实现这个玩意儿,数组或者链表。
- java里的queue是个interface。 有用Array来实现的,比方说ArrayBlockingQueue。还有个常用的类implements了这个interface,就是刚刚说到的LinkedList。
1.6 Deque(双向队列)
就是两头都可以进出的queue,可以从队列前面添加删除,也可以从队列后面添加删除。
- java提供了一个Interface Deque,它是Queue的孩子,LinkedList也implement了它。
import java.util.*;
public class DequeExample
{
public static void main(String[] args)
{
// LinkedList implements了它
Deque<String> deque = new LinkedList<String>();
// 有七个方法用来添加element
deque.add("1 (Tail)"); // 从后面添加
deque.addLast("2 (Tail)");
deque.offer("3 (Tail)");
deque.offerLast("4 (Tail)");
deque.addFirst("5 (Head)"); //从前面添加
deque.push("6 (Head)");
deque.offerFirst("7 (Head)");
// 可以从头到尾遍历一遍
System.out.println("Standard Iterator");
Iterator iterator = deque.iterator();
while (iterator.hasNext())
System.out.println("\t" + iterator.next());
//也可以从尾到头遍历一遍
System.out.println("Reverse Iterator");
Iterator reverse = deque.descendingIterator();
while (reverse.hasNext())
System.out.println("\t" + reverse.next());
}
}
1.7 Stack(栈)
只需要记住四个字:先进后出!!!
记不住怎么办,stack作动词的时候是堆放的意思。堆东西的时候当然是堆在最上面的时候取的时候最先取啊。
图片来源: https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
- 添加数据的时候加到最后面,取的时候也从最后面一个取。(刷过盘子没,就是这道理)
- 一般来说一个Stack一定要有这几个方法, 这几个方法时间复杂度都是O(1)
- push(): 添加一个元素
- pop(): 从stack里移除最上面的元素
- peek()或者top(): 返回最上面的元素
- isEmpty(): 是不是空的。
- Java里Stack是个Class。 而且是concrete class。它是Vector的孩子,Vector是List的孩子。
import java.util.*;
public class StackDemo {
public static void main(String args[]) {
Stack st = new Stack();
st.push(new Integer(1));
st.push(new Integer(2));
st.push(new Integer(3));
System.out.println("stack: " + st); //stack: [1, 2, 3]
Integer a = (Integer) st.pop();
System.out.println(a); //3
System.out.println("stack: " + st); //stack: [1, 2]
}
}
1.8 HashTable(哈希表/散列表)
HashTable勉强算一个线性数据结构。本质上,是一个array,但是array里的每个格子存的都是一个linkedList而已。
图片来源: https://www.hackerearth.com/practice/data-structures/hash-tables/basics-of-hash-tables/tutorial/
- 把数据映射到一个array index的过程叫做hashing,这个计算叫做hash function。比方说: 要存放{11,13,14,15, 21}到一个hashtable里面去,我们的hash function用key%10,11%10=1,所以11放到index为1的格子里。 13%10=3,所以13放到index为3的格子里。
- 优点: 查找快,如果想查找13在哪里,直接找index为3的格子就好。
- 缺点:浪费空间,可能有很多格子是空的。
- 在上面的例子中,11和21都映射到了第一个格子,所以1号格子里的linkedList就有了两个数据,11和21。但是如果把hash function变成key%20,那又会造成很多空间浪费。
- 所以说,hash function要好好设计,要尽量的避免重复的映射。又要避免空间浪费。
- HashTable这种数据结构在Java里面有很多个class。比较常用的有三个,都姓Hash。他们分别是,HashTable,HashMap,HashSet. 其中HashTable,HashMap存储的数据是<key,value>对,每个hash function是对key进行计算的,HashSet存储的是value。
- 上面这三个在Java里都是Class。来个例子:
import java.util.*; class HashDemo { public static void main(String[] arg) { Hashtable ht = new Hashtable(); ht.put("hta", 1); ht.put("htb", 2); ht.put("hta", 3); //一个key只能对应一个值 //ht.put(null, 4); hashtable不允许null key System.out.println(ht); //{htb=2, hta=3} HashSet<String> hs = new HashSet<String>(); hs.add("hsa"); hs.add("hsb"); hs.add("hsa");//只能有一个value System.out.println(hs); //[hsb, hsa] HashMap<String, Integer> hm = new HashMap<>(); hm.put("hma", 1); hm.put("hmb", 2); hm.put("hma", 3); hm.put(null, 4); //hashMap可以有null值 System.out.println(hm); //{null=4, hmb=2, hma=3} } }
2.Tree 树
非线性结构的逻辑特征是一个结点元素可能对应多个直接前驱和多个后继。
非线性结构包括:
- 图(群结构)
- 树(层次结构)
- 多维数组
其实如果把一个array看作是一个元素的话,多维数组应该也算是线性结构吧。。。。
图太好玩儿了,而且java里面没有对应的类或者是interface,这里就不说了。
只简单看看树就好了。
图片来源: https://codepumpkin.com/tree-data-structure-terminologies-set-1/
关于树,需要了解的东西
- 二叉树
- 二叉查找树
- 平衡二叉树
- 平衡查找树
- 最大堆,最小堆
- 红黑树 – 用来排序的
- 深度优先搜索
- 广度优先搜索
Java里只有两个和Tree有关的Class: TreeMap和TreeSet。 只要是名字带tree的,都是排序好了的。
3.Set
Set是不允许有重复数据的,只要是XXSet,说明存储的数据中,key是不能重复的。
4.Collection家谱
图片来源:http://www.javawebtutor.com/articles/corejava/java_collections.php
最后来看面试官喜欢的问题: 给出一个情况,问你要用什么来存储数据
- 数据类型是什么
- 存储的数据是需要排序的么
- 存储的数据允许重复么
- 这些数据会经常增删么
- 这些数据会被频繁的访问么