目录
了解数据结构吗?说一下常见的数据结构?xysj面试
1 常见的数据结构
1.1 数组(Array)
基本概念
数组是一种线性表数据的结构,他用一组连续的内存空间,来存储一组相同数据类型的数据。每一个数组元素的位置由数字编号,称为下标或者索引(index)。大多数编程语言的数组第一个元素的下标是 0。
- 线性表:数据排列成一条线一样的结构。数据结构特点:存在一个唯一的没有前驱的(头)数据元素;存在一个唯一的没有后继的(尾)数据元素存在头和尾元素。像队列,链表,栈也是线性表结构。对应的还有非线性表结构(数据没有先后顺序的,二叉树,堆等)
- 连续内存空间:计算机在分配内存空的时候都会对应分配一个内存地址,连续的内存空间对应的是指连续的内存地址,计算机是通过访问内存地址会获取内存中的值。
- 相同的数据类型:相同的数据类型,换句话可以说数据存储所占用内存大小一样
- 根据维度区分,有 2 种不同的数组:
- 一维数组(如上图所示)
- 多维数组(数组的元素为数组)
- 数组的基本操作
- Insert - 在某个索引处插入元素
- Get - 读取某个索引处的元素
- Delete - 删除某个索引处的元素
- Size - 获取数组的长度
特性
- 随机访问
基于上面的概念描述,下面来分析一下数组的最大特性:随机访问
非随机访问:就是存取第N个数据时,必须先访问前(N-1)个数据 (链表)
随机访问:就是存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作(数组)
如下图所示:
- 为什么数组下标都是从0开始?
从上面图示我们来分析:
- 假设下标为1开始:我们要想获取第3个值得话 首地址(1000)+ (3-1)*4(数据类型占用的内存) = 1008 第三个内存地址的位置
- 假设下标从0开始:我们想获取第3个值得花 首地址(1000)+ 2 *4(数据类型占用的内存) = 1008 省去了一个减的动作 提高了访问的效率。
1.2 栈(stack)
我们可以这样认为栈(Stack)是一种特殊的线性表,其插入和删除操作只允许在线性表的一端进行,一般而言,把允许操作的一端称为栈顶(Top),不可操作的一端称为栈底(Bottom),同时把插入元素的操作称为入栈(Push),删除元素的操作称为出栈(Pop)。若栈中没有任何元素,则称为空栈,栈的结构如下图:
栈的基本操作
- Push——在顶部插入一个元素
- Pop——返回并移除栈顶元素
- isEmpty——如果栈为空,则返回true
- Top——返回顶部元素,但并不移除它
1.3 队列(Queue)
与栈相似,队列是另一种顺序存储元素的线性数据结构。栈与队列的最大差别在于栈是LIFO(后进先出),而队列是FIFO,即先进先出。
一个完美的队列现实例子:售票亭排队队伍。如果有新人加入,他需要到队尾去排队,而非队首——排在前面的人会先拿到票,然后离开队伍。
队列的基本操作
- Enqueue()——在队列尾部插入元素
- Dequeue()——移除队列头部的元素
- isEmpty()——如果队列为空,则返回true
- Top()——返回队列的第一个元素
1.4 链表(Linked List)
链表是另一个重要的线性数据结构,有点像数组,但在内存分配、内部结构以及数据插入和删除的基本操作方面均有所不同。
链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。 链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。
链表一般用于实现文件系统、哈希表和邻接表。
链表分为 2 种:
- 单向链表
- 双向链表
链表的基本操作
- InsertAtEnd — 在链表结尾插入元素
- InsertAtHead — 在链表开头插入元素
- Delete — 删除链表的指定元素
- DeleteAtHead — 删除链表第一个元素
- Search — 在链表中查询指定元素
- isEmpty — 查询链表是否为空
1.5 图
图(graph)由多个节点(vertex)构成,节点之间阔以互相连接组成一个网络。(x, y)表示一条边(edge),它表示节点 x 与 y 相连。边可能会有权值(weight/cost)。
1.6 树
树(Tree)是一个分层的数据结构,树是一种非线性的数据结构,是由n(n >=0)个结点组成的有限集合。树是一种特殊的图,它与图最大的区别是没有循环。如果n==0,树为空树。如果n>0,树有一个特定的结点,根结点只有直接后继,没有直接前驱。
树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。
树的度
树的结点包含一个数据和多个指向子树的分支
结点拥有的子树的数量为结点的度,度为0的结点是叶结点,度不为0的结点为分支结点,树的度定义为树的所有结点中度的最大值。
二叉树(Binary Tree)
二叉树是由n(n>=0)个结点组成的有序集合,集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。
满二叉树
如果二叉树中所有分支结点的度数都为2,并且叶子结点都在统一层次上,则二叉树为满二叉树。
红黑树(Binary Tree)
红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
旋转包括两种:左旋 和 右旋。
左旋示例图(以x为节点进行左旋):
对x进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将 x变成了一个左节点(x成了为z的左孩子)!。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。
右旋示例图(以x为节点进行右旋):
对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点(x成了为y的右孩子)! 因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”。
1.7 散列表(哈希表)
xxx
2 那你说一下数组和链表的区别吧?
数组
一、数组的特点
1.在内存中,数组是一块连续的区域
2.数组需要预留空间
- 在使用前需要提前申请所占内存的大小,这样不知道需要多大的空间,就预先申请可能会浪费内存空间,即数组空间利用率低
- 数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)
3.在数组起始位置处,插入数据和删除数据效率低。
- 插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移
- 删除数据时,待删除位置后面的所有元素都需要向前搬移
4.随机访问效率很高,时间复杂度可以达到O(1)
- 因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址处向后偏移就可以访问到了
5.数组开辟的空间,在不够使用的时候需要扩容,扩容的话,就会涉及到需要把旧数组中的所有元素向新数组中搬移
6.数组的空间是从栈分配的
二、数组的优点
随机访问性强,查找速度快,时间复杂度为O(1)
三、数组的缺点
1.头插和头删的效率低,时间复杂度为O(N)
2.空间利用率不高
3.内存空间要求高,必须有足够的连续的内存空间
4.数组空间的大小固定,不能动态拓展
链表
一、链表的特点
1.在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续
2.链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址
- 每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据
3.查找数据时效率低,时间复杂度为O(N)
- 因为链表的空间是分散的,所以不具有随机访问性,如要需要访问某个位置的数据,需要从第一个数据开始找起,依次往后遍历,直到找到待查询的位置,故可能在查找某个元素时,时间复杂度达到O(N)
4.空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
5.任意位置插入元素和删除元素效率较高,时间复杂度为O(1)
6.链表的空间是从堆中分配的
二、链表的优点
1.任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
2.内存利用率高,不会浪费内存
3.链表的空间大小不固定,可以动态拓展
三、链表的缺点
随机访问效率低,时间复杂度为0(N)
综上:
对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表
3 其他
-
创建多线程有哪些方法?
在JDK1.5之前,创建线程就只有两种方式,即继承java.lang.Thread类和实现java.lang.Runnable接口;之后开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口。- 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法。
- 自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
- 自定义类实现Callable接口并重写call方法,创建该类的对象作为实参来构造FutureTask类型的对象(用于接收线程运算结果),然后使用FutureTask来构造Thread类型的对象去调用start方法。
- 线程池来实现,线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提交了响应速度。步骤:创建线程池,执行任务void execute(通常用于执行Runnable)/ Future submit(通常用于执行Callable),最后关闭线程池shutdown() 。
-
讲一下run()和start()
start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法。若直接调用run()方法,系统run()方法会立即执行,但是这时候系统会把run()方法当成普通的方法,线程对象也当成一个普通对象。就不能达到创建一个新的任务的目的。
run没有启新的线程,start方法才会调用Thread的native的start0方法,start0会调用run方法,开启新的线程 -
数据库用过什么?了解数据库的索引和锁吗?
-
了解设计模式吗?说一下?观察者模式说一下?
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
//创建主题对象
ConcreteSubject subject = new ConcreteSubject();
//创建观察者对象
Observer observer = new ConcreteObserver();
//将观察者对象登记到主题对象上
subject.attach(observer);
//改变主题对象的状态
subject.change("new state");
在观察者模式中,又分为推模型和拉模型两种方式。
- 推模型:主题对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。
- 拉模型:主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。一般这种模型的实现中,会把主题对象自身通过update()方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
java内置观察者模式:
被观察者对象 extends Observable
观察者对象 implements Observer