时间复杂度和空间复杂度
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
时间复杂度是相对的,相对于别的算法优劣,并不是实际执行时间。
复杂度计算的时候,直接取最大可能的情况,舍弃小的。
如 n²+1000n+100000 直接就取n²
1.常量,不管语句多长都是Ο(1)
2.一个for循环复杂度为Ο(n)
3.两个for嵌套复杂度为Ο(n²)
依次类推
复杂度Ο(n³)
O(log2n)
Ο(n²)
数组
1. 如何在一个1到100的整数数组中找到丢失的数字?
有序直接二分查找判断下标与i的大小,无序所有数相加-本来应该多大
2. 如何在给定的整数数组中找到重复的数字?
用一个Map记录重复数,如果不重复就添进去,map里已经有了就下一个。
3. 给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。要求:空间复杂度O(1),时间复杂度为O(n)
两个指针,一个从左往右,一个从右往左。
从左往右扫描,左边如果遇到了偶数。那就从右往左扫描,如果遇到奇数,两个指针位置处的数字交换。
4. 如何在未排序整数数组中找到最大值和最小值?
用两个变量同时保存最大和最小值,然后用for循环遍历整个数组,记录最大最小值。
5. 在Java中如何从给定排序数组中删除重复项? 只能在原数组操作
输入: nums = [0,0,1,1,1,2,2,3,3,4]
输出: 5, nums = [0,1,2,3,4]
两个指针,一个for循环的指针,代表遍历到哪了,一个记录所有不重复的位置。
如果遇到重复的,那就慢指针不走了,快指针继续走。直到遇到不重复的,快指针处的值赋值到慢指针处。
不重复就两个指针一起走,重复慢指针就不走。
[0,0,1,1,1,2,2,3,3,4]
一个指在0号位,一个指在1号位。开始,重复下一次for。
一个0一个2,不重复,repeat++,一个1一个2,1号位赋值成2号位。
一个1一个3,重复
1,4 重复
1,5 不重复 1++ 2号位赋值成5号位
链表
1. 一个数组插入删除查找和链表的效率对比?如果一个数组要反复插入删除怎么优化降低时间复杂度?
参考ArrayList和linedlist,数组查询快(可以从下标计算内存存储的位置)增删慢,删完以后后面得全部移动。链表查询得遍历查慢,增删只要把引用改一下就行,快。
标记清除算法。标记完统一删除。
2. ArrayList、LinkedList复杂度
ArrayList查询第一个跟最后一个复杂度一样么?
一样
那LinkedList查询第一个跟最后一个复杂度一样么?
LinkedList 是双向的,查询第一个跟最后一个是一样的,但是第一个和最后第二个就不一样。
第一个直接给了头结点,倒数第二个需要从倒数第一个开始查询,走两步
3. 如何得到单链表的长度?
while循环,next一个一个向后找
4. 如何逆转单链表?
- 递归
递归到最底层,如果下一个node=null
将最后一个node.next->指向他的前面一层,返回递归
4号位置指向node.next->指向他的前面一层
入参为当前节点和他的上一步的节点
- 迭代
新建一个node用来存放完成的链表。
1.while循环 先存在next下一个节点 2345
2.卸下第一个节点(让他1.next=null -- 等于完成的)
3.将1节点存到完成列表 1
4.全部节点变成next12345变成2345
5.下一次循环 卸下2节点
...
5. 如何在一次遍历中找到单个链表的中间节点的值?
两个指针,一个走两步,一个走一步if(all%2 == 0)
6. 如何证明给定的链表是否包含循环?如何找到循环的头节点?
是否循环
定两指针,快指针走两步,慢指针走一步,如果两个指针相遇了,那就说明有环了。
寻找循环头
相遇点/起点各设一个指针,一次一步,相遇点就是循环头。
7. 两个有交叉的单链表,求交叉点
1.遍历链表A存到HashSet。遍历链表B,判断有无已经存到哈希表里了。
2.定两指针,一个从A开始,一个从B开始,每次都移一位,向后移。到末尾,A从B开始向后移。B从A开始向后移。两条路的交点就是交叉点。
因为这三段加起来是固定值。所以这两段是一样长的。
8. 如何找链表倒数第n个元素?
双指针,第一个指针先走n个位子,然后两个指针一起走,当n走到尾的时候。第二个指针的位置处就是倒数第n个元素。
9. 求矩阵中连续1的个数 Number of Islands
10. 大数相加
用链表存储,操作。
逆序首先想到栈。用栈将两个链表存进去,那么取出来的时候就是逆序的了,然后相加就行。
队列&堆栈
LinkedList 双向链表。除了当做链表使用外,它也可以被当作堆栈、队列或双端队列进行操作。
栈
Deque<Integer> stack1 = new LinkedList<Integer>(); Deque双端队列,所以可以实现先进后出
入栈:stack1.push();
出栈:stack1.pop();
返回栈头:stack1.top;
是否空:stack1.empty();
栈添进去的时候,就自动给逆序了
队列
Queue <Integer> queue1 = new LinkedList<>();
添:queue1.offer();
取:queue1.poll();
返回队头:queue1.peek();
1. 对比一下队列和栈,以及它们底部实现
队列先进先出,双端操作,像排队。
栈先进后出,一端操作,像往罐子放东西。
栈实现:顺序栈、链式栈。
顺序栈:用数组实现。定义一个数组,max=栈容量,一个指针代表现在栈内存了几个。
push:往数组上添一个数据,如果数组满了,那么return提示或者扩容数组
pop:顶端取出一个值
操作得加锁!!!!
链式栈:用链表实现栈。只需要操作链表头就行,入栈就是往列表头添一个节点(新节点.next=原链表),出栈就是在头取出一个节点。
在记录一下链表的长度
2.队列实现:顺序队列、链式队列。
顺序队列:用数组实现,因为是先进先出的,a[]进i=1234,出i=1,下次进i=5;这样就造成了下面为空的情况
为了解决这个问题,可以定两指针,一个指队列头,一个指队列尾。当数组i=5存满时,可以通过6%6存到i=0出,这样形成了一个循环
下一个存这
指针重合说明队列为空
(尾+1)%MaxSize == 首 说明队列满
链式队列:定义两个指针,队首、队尾。入队列在队尾添,出队列在队首拿。
入:队尾.next=new
出:直接取走队首
2. 队列实现栈
用两队列实现栈。
关键就在逆序存储,队列逆序就是栈
3. 栈实现队列
stack1.push栈添进去的时候自动就逆序了,取得时候逆回来,然后返回队头就行。
二叉树
概念:二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。
(自己总结:二叉树为子节点最多2个的树,儿子节点称为左子树、右子树,子节点顺序不能颠倒)
结点的度:儿子节点的个数。
深度/层数:根为1层,有几层
左子树、右子树:二叉树左右儿子,有顺序要求,不能颠倒
特点:
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
性质:
1)在二叉树的第i层上最多有2i-1 个节点 。(i>=1)
2)n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
3)在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
4)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
斜树
满二叉树
满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
完全二叉树
完全二叉树:对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
(跟满二叉树的区别就是自右往左少几个。排是按照从上到下,从左到右排列的就是完全二叉树)
平衡二叉树
平衡二叉树又被称为AVL树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
(总结:一层放满再往下一层放)
查找速度跟层数有关,这种方式层数少,所以找得快。但是插入、删除的时候,为了保持平衡性,得一直旋转树,比较慢。
这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
二叉树的存储结构
顺序存储
完全二叉树这样存比较好,但是如果这个树是乱的。
圈出来的没有
存完以后就这样了
二叉链表
在链表里(本来存上一个节点,下一个节点改成存左子树和右子树)
二叉树的遍历
前序遍历:父>左>右 从根出发一直遍历到最左边,然后往上往右遍历 ABDHIEJCFG
中序遍历:左>父>右 从最左的子树开始,左中右遍历 HDIBJEAFCG
后序遍历: 左>右>父 从最左的子树来时,左右中遍历 HIDJEBFGCA
层序遍历: 自上而下,自左而右。跟完全二叉树遍历一样 ABCDEFGHIJ
红黑树
红黑树是一种含有红黑结点并能自平衡的二叉查找树。
B树、B+树
B树:B-tree - 平衡树 - 平衡的儿子节点>2的树
(1)排序方式:所有节点按递增排列,左小右大;
(2)子节点数:1<x<=M,(1,M],M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);
(3)关键字数:ceil(m/2)-1 <= 关键字数 <= (M-1)
(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);
(4)所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;
查找
找E。
1.E<M所以在左子树找
2.D<E<G所以在中间儿子找
3.E=E,返回E节点。没有的话返回null
插入:关键字数 <= (M-1),5阶最多4个,多了就要把中间的树提升为父节点,左边变左子树,右边变右子树
3、8、31、11、23、29、50、28构建5阶树。
5阶树,所以关键词最大数是4。
先插入3、8、31、11
在插入23、29
在插入50、28
删除:关键词数>=ceil(m/2)-1,5阶就是少于2个就要合并了。少了先从儿子取,取不到再从父亲取。
特点:B树相对于平衡二叉树,每个节点包含的关键字增多了。
特别是用到数据库中,充分利用了磁盘块的原理。(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;
B+树
B+树是B树的一个升级版,B+树查找的效率要比B树更高、更稳定。其速度完全接近于二分法查找。
1.B+树根(枝)节点不存数据、指针,只存索引。这样非叶子节点存的关键词更多
2.叶子节点存了父节点的关键词信息,数据全存在叶子节点
3.B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
4.非叶子节点的子节点数=关键字数
特点:
1.层级更少、查询速度快:每个非叶子节点存储的关键字数更多。
2.查询速度更稳定:数据都存在叶子节点,所以查询速度是一样的。
3.叶子节点是有序链表,查询更快,缓存命中率更高。
4.可以只扫描叶子节点,全数据扫描,更快。
B*树
枝节点存有兄弟的指针,满了可以往兄弟那添,减少分裂次数。
总结
1、相同思想和策略
从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度;
2、不同的方式的磁盘空间利用
不同点是他们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的;
二叉树题
1. 前序遍历为{1,2,4,7,3,5,6,8},中序遍历为{4,7,2,1,5,3,8,6}
前序:父>左>右 第一个节点为根
中序:左>父>右 1在中序的位置472 为左子树 5386为右子树
前序:2为1的左子树的根 中序2在1边上 所以为47-2。再看前序4在前面,所以4是根7是子
2. 先序遍历的实现、中序后序同理
先序遍历:根>左>右。树的遍历都是递归。
先将自己添加到list在递归添左,递归添右
3. 输入两棵二叉树 A 和 B,判断 B 是不是 A 的子结构。
都先序遍历,看有没有重复的一串树。
leetcode-cn.com/problems/ch…
4. 任意一颗二叉树,求最大节点距离
有两种情况:
情况A: 路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
情况B: 路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者
A:直接左子树层数+右子树层数
B:需要知道左子树的最远距离,右子树的最远距离。
5. 请实现两个函数,分别用来序列化二叉树和反序列化二叉树
leetcode-cn.com/problems/xu…
1.序列化:递归,先序遍历将树遍历成List,空的为null
2.反序列化:也是递归。传入list,先new自己节点,放入list(0),在左子树放左边,右子树放右边,先左后右。
图
旋转矩阵
- 用辅助数组
new 一个空数组,然后一一对应放入。最后将旋转后的数组,复制回来就行。
- 原地旋转
先存下(i,j)处的值,然后一个等于一个,螺旋赋值,这样赋值一圈以后,这些位置处的数就已经旋转完事了。
总共要旋转(n/2)圈,有几层旋转几圈,偶数直接n/2,奇数最中间的不需要动,所以也是(int)n/2取整。
x轴遍历:如第一圈最后一个是不用遍历的,第二圈最后两。也就是j<n-i-1
矩阵,左到右,上到下都是递增,找一个数
左到右,上到下都是递增,找一个数,输入8true,输入5false
1、分治法,分为四个矩形,配以二分查找,如果要找的数是6介于对角线上相邻的两个数4、10,可以排除掉左上和右下的两个矩形,而递归在左下和右上的两个矩形继续找,如下图所示:
2、定位法,时间复杂度O(m+n)。首先直接定位到最右上角的元素,再配以二分查找,比要找的数(6)大就往左走,比要找数(6)的小就往下走,直到找到要找的数字(6)为止,如下图所示:
排序算法
1. top-k排序(堆排序,位图法)
选出最大的几个数。
1.直接排序,排完取最大的几个。效率低下
2.冒泡排序,第一个循环选出最大值,多循环几次就选出了最大的几个数。局部排序,效率稍微好点。
3.堆排序,只找最大的topK不排序。
前k个元素生成一个小顶堆,用来存最大的几个数
然后向后遍历,对比堆顶的数,如果大,那就替换堆顶的数。这样遍历完事,堆上的数字就是最大的几个数。
冒泡排序
两个for循环,一一比较,A比B大就交换,这样一趟以后就选出了一个最大值。
时间复杂度:O(n²)
堆排序
堆排序 - 选择排序,不稳定的排序,运气不好,得一直交换。
www.cnblogs.com/chengxiao/p…
时间复杂度:O(n*log n)
1.先建立一个大顶堆(节点跟自己的儿子节点比较,把最大的交换到母节点。这样遍历完,根节点就是最大的节点)(自己节点是i,左子树=2i+1;右子树=2i+2)
遍历完事以后,根是最大的数
2.将根节点与最尾巴的叶子节点交换,这样最大的值就在末尾了,排序好了一个。然后在遍历一次就第二大在倒数第二个位置。。。。
快速排序
定一个基准值,将比他小的放左边,大的放右边。然后递归左右两数组。
定一个基准值,然后两个指针(从前往后扫,从后往前扫)。前指针比较是否比基准大,后指针比较是否比基准小。如果前面值比基准大,后面值比基准小,那就交换。这样一轮以后,基准值左边的都比他小,右边的都比他大。(在将左右两边分别排序)
自己实现的,如果不重复可用:
插入排序
时间复杂度:O(N)~O(N²) 空间复杂度:O(1)
有点像冒泡排序,往后遍历。一个数,比较他前面所有的数,插入到正确的顺序中。
查找算法
有序数组二分查找
leetcode-cn.com/problems/bi…
时间复杂度O(logN)。
空间复杂度:O(1)。
n人排成圈报数,报到3的退出,循环直至最后一个,最后一个的原来号码是多少
串
无重复最长串
用HashSet去存字符,定义左右指针,遍历没遇到重复就添进去并将右指针后移。遇到重复就左指针向右移,并且Hash移除字符。 遍历完事,存下最大的长串。