左神数据结构和算法(基础)
1.简单排序算法
异或方法交换两值
一个很有意思不需要开辟新缓冲的方法——通过异或运算实现两数交换
首先我们需要了解异或的性质:
- a^0 = a a^a=0
- 满足交换律和结合律
1 | //三轮a异或b实现两数交换 |
这里a和b必须满足两者指向两块不同的内存区域,否则这种方法会使两值都变为0
异或思考题
要求:时间复杂度O(n);空间复杂度O(1)
在数组arr中
(1)有一种数出现了奇数次,其余都是偶数次,如何找到这个数
解析:定义一个数eor=0,将其和数组中所有数异或得到的数即为奇数次的这个数。
1 | public int questionOne(int[] arr){ |
(2)有两种数出现了奇数次,其余都是偶数次,如何找到这两个数
解析:设数组中奇数次数是a和b,定义eor=0,将其和数组所有元素异或后得到eor=a^b
由于a!=b,所以eor至少有一位不为0,说明a和b有一位不一样
假设第8位不一样,将数组中的元素按照第八位是0或1分为两组,a和b肯定分别在这两个组中
定义eor’遍历异或其中一组即得到a或者b,接着eor^eor’即得到另一个数
1 | public void questionTwo(int []arr){ |
选择、冒泡、插入排序
选择排序:时间复杂度O(n^2) 空间复杂度O(1)
1 | public void selectionSort(int[] arr){ |
冒泡排序 时间复杂度O(n^2) 空间复杂度O(1)
1 | public void bubbleSort(int[] arr){ |
插入排序和数组顺序有关,最好的情况下时间复杂度O(n),最坏的情况下则是O(n^2),因此插入排序比冒泡和选择排序要好一点
1 | public void insertionSort(int []arr){ |
2.O(logn)的排序
master公式
题目:求出一个数组中元素的最大值
1 | public int araayGetMax(int[] arr){ |
这种运用了递归的算法我们求时间复杂度时,我们通常用到master公式
master公式: T(n) = a * T(N/b) + O(N^d);
当:
log b A < d 时,程序的时间复杂度为:O(N^d);
log b A > d 时,程序的时间复杂度为:O(N^log b A);
log b A = d 时,程序的时间复杂度为:O(N^d * log N);
符合子问题规模是等规模的行为,均可使用master公式求解时间复杂度。
如上述程序我们在求一个数组长度为N的最大值时,其中
a: 子问题调用的次数,本程序的次数为2次(leftMax和rightMax);
N:为母问题规模,本程序的母问题规模为N;
T(N/b): 子问题规模,本程序的子问题规模为:N/2(二分);
O(n^d): 除子问题调用之外的时间复杂度,本程序为O(1);
可得本程序的master公式为:T(N) = 2 * T(N/2) + O(1);其中a=2,b=2,d=0
所以,本程序的时间复杂度为:O(N)
归并排序
1 | public void mergeSort(int[] arr){ |
分析时间复杂度,子问题等规模,可以用master公式:T(N) = a*T(N/b) + O(N^d)
此处master公式为:T(N) = 2T(N/2) + O(N),其中a=2,b=2,d=1,因此时间复杂度是O(N\logN)
小和问题
在一个数组中,每一个元素左边比当前元素值小的元素值累加起来,叫做这个数组的小和
例如:[2,3,4,1,5]
2左边比2小的元素:无
3左边比3小的元素:2
4左边比4小的元素:2,3
1左边比1小的元素:无
5左边比5小的元素:2,3,4,1
小和small_sum = 2 + 2 + 3 + 2 + 3 + 4 + 1 = 17
分析:
上述举例可以转化成该元素右边有多少比自己大的元素的个数,小和=每个元素数值*每个元素求出的这个个数。
如上述例子中,2右边比它大的元素有3个,3右边有2个,4右边有1个,1右边有一个,5右边没有,最终小和=2*3+3*2+4+1=17
我们可以用归并排序的特点来解决此问题,小和只有在merge的过程中才会产生,并且小和只有在左边的数字小于右边的数字的时候才会产生小和,因为在merge过程中,左右两边已经有序,所以在本次merge过程中该数字产生小和的个数可以用下标计算得出。一个有序数组内不产生任何小和(因为我们已经在之前的merge过程中计算过了)。这样做我们就不会把小和重复计算。
1 | public int smallSum(int[] arr){ |
逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。
1 | public void reverseOrder(int[] arr){ |
荷兰国旗问题
问题一:
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
分析:
问题二:
给定一一个数组arr和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
快速排序
平均时间复杂度O(N*logN),空间复杂度O(logN)
1 | public void quickSort(int[] arr){ |
3.堆排序和排序总结
堆排序
堆排序用到了大顶堆的概念,之前的博文中已经介绍,想看的朋友自行翻阅《堆,图,字符串匹配算法超详细讲解》这篇。
1 | public void heapSort(int[] arr){ |
堆排序扩展题目
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。
请选择一个合适的排序算法针对这个数据进行排序。
分析
我们可以利用小顶堆来进行排序,假设k=6,我们一开始先将数组下标为0~6的元素放入小顶堆,小顶堆弹出的第一个元素就是最小的数放入0位置(由于最大距离为6,因此最小的肯定是数组中最小的),接着将7号元素放入小顶堆,小顶堆弹出的放入1号位置,循环往复…………
1 | public void process(int[] arr,int k){ |
排序总结
面试题:在Arrays的sort方法中,面对基础类型的默认使用快排,面对用户自定义的类型默认选择归并,这是为什么?
基于稳定性的考虑,基础类型的话稳定性不重要,而用户自定义的系统不知道需不需要稳定性,索性直接用稳定的归并排序算法。
4.链表
哈希表和有序表
Hash表简单介绍:
- 有无伴随数据,是HashMap和HashSet唯一的区别,底层的实际结构是一回事
- 使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为O(1),但是常数时间比较大
- 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
- 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小
有序表简单介绍:
有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事
有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织,时间复杂度也稍大一点是O(logN)
红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
有序表固定操作:
1)void put(K key,V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value。
2)V get(K key):根据给定的key,查询value并返回。
3)void remove(K key):移除key的记录。
4)boolean containsKey(K key):询问是否有关于key的记录。
5)K firstKey():返回所有键值的排序结果中,最左(最小)的那个。
6)K lastKey():返回所有键值的排序结果中,最右(最大)的那个。
7)K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的前一个。
8)K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的后一个。以上所有操作时间复杂度都是O(logN),N为有序表含有的记录数
链表简单题
【题目】分别实现反转单向链表和反转双向链表的函数
【要求】如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)
1 | //反转单向 |
【题目】给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
【要求】如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)
1 | public static void function(Node head1,Node head2){ |
判断回文结构
【题目】给定一个单链表的头节点head,请判断该链表是否为回文结构。
【例子】1->2->1,返回true;1->2->2->1,返回true;15->6->15,返回true;1->2->3,返回false。
【要求】如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
先说不考虑空间复杂度为O(1)的情况,我们可以通过栈来方便的判断链表是否为回文结构。
1 | //栈,需要额外空间,空间复杂度O(N) |
如果需要通过有限几个变量来实现判断,我们需要用快慢指针确定中点位置后将后面的链表反转,和左边的逐一比较,比较完后需要将反转的链表复原。
1 | //need O(1) space |
链表荷兰国旗问题
【题目】
给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。
解题思路:将链表复制到数组中,利用荷兰国旗问题思想解决,需要额外N空间
1 | //方法一:将链表复制到数组中,利用荷兰国旗问题思想解决,需要额外N空间 |
【进阶】在实现原问题功能的基础上增加如下的要求
【要求】调整后所有小于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有等于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有大于pivot的节点之间的相对顺序和调整前一样
【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
解题思想:分为三个链表,依次插入,最后链接起来。
1 | public static Node function2(Node head,int pivot){ |
复制含有随机指针的链表
【题目】
一种特殊的单链表节点类描述如下
1 | public class Node { |
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度O(N),额外空间复杂度O(1)
1 | //方法一:通过map结构,key存储源节点,value存储复制节点,复制节点的指向都可以通过map结构得到 |
两单链表相交的一系列问题
【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null
【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
1 | //找到链表第一个入环节点,如果没有返回null |
5.二叉树
递归实现二叉树遍历
首先我们举一个简单的例子介绍递归序
想遍历这个二叉树我们写一个简单的递归
1 | public void function(Node head){ |
递归序:1,2,4,4,4,2,5,5,5,1,3,6,6,6,3,7,7,7,3,1
再分别看看我们的先序遍历,中序遍历,后序遍历:
先序(头左右):1,2,4,5,3,6,7
中序(左头右):4,2,5,1,6,3,7
后序(左右头):4,5,2,6,7,3,1
我们发现一个规律:先序遍历就是在递归序中的元素第一次出现顺序的时候输出,中序是第二次,后序是第三次
1 | //递归先序遍历 |
非递归实现二叉树遍历
先序遍历:先把头节点放到栈里,接着固定流程
- 每次从栈中弹出一个节点记为cur
- 打印(处理cur)
- 把cur的右节点先放入栈中,再把左节点放入栈中(如果有的话)
- 循环往复
1 | public static void preOrderUnRecur(Node head){ |
后序遍历:创建两个栈,一个普通栈,一个收集栈,先把头节点放到普通栈里
- 每次从普通栈中弹出一个节点记为cur
- 将cur放入收集栈中
- 先左再右(如果有的话)
- 循环往复
分析:这样子普通栈里面弹出的顺序是头右左,则收集栈里面弹出的顺序就是左右头,也就是后序遍历
1 | public static void posOrderUnRecur(Node head){ |
中序遍历:
先整棵树的左边界进栈,依次弹出的过程中打印出来,接着对弹出节点的右子树作上述操作循环往复。
1 | public static void inOrderUnRecur(Node head){ |
求一棵二叉树的宽度
1 | public static int getTreeMaxWidth(Node head){ |
二叉树的相关概念及其实现判断
如何判断一颗二叉树是否是搜索二叉树?(搜索二叉树:每个子树的左节点都比头节点小,右节点都比头节点大)
将二叉树中序遍历,应该得到的是一个递增的数列。
1 | public static int cur = Integer.MIN_VALUE; |
如何判断一颗二叉树是完全二叉树?
(1)任意节点有右无左返回false
(2)在(1)不违规的条件下,如果宽度遍历遇到了第一个左右子树不全的节点,后续的节点都是叶节点
1 | public static boolean isCbt(Node head){ |
如何判断一颗二叉树是否是满二叉树?
满二叉树满足条件:深度h和节点个数n关系——n=2^h-1
1 | public static class ReturnType{ |
如何判断一颗二叉树是否是平衡二叉树?
平衡二叉树:任意节点的左右两个子树高度差不超过1
1 | public static class ReturnType{ |
最低公共祖先节点
给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点
1 | //o1,o2属于以head开头的树,找到o1和o2最低公共祖先节点 |
后继节点
后继节点:在二叉树的中序遍历的序列中,node的下一个节点叫作node的后继节点。
【题目】现在有一种新的二叉树节点类型如下:
1 | public static class Node{ |
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。
假设有一棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。
方法:分有右子树和没有右子树两种情况
有右子树:后继节点是右子树最左边的节点
没有右子树:后继节点是向上遍历是某个父节点的左节点的那个父节点,一直找不到就是整个树最右边的节点,指向null
1 | public static Node getSuccessorNode(Node node){ |
二叉树的序列化和反序列化
序列化:内存里的一棵树如何变成字符串形式
反序列化:从字符串形式变成内存里的树
1 | public static String serializeByPre(Node head){ |
折纸问题
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。
此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印所有折痕的方向。
例如:N=1时,打印:down N=2时,打印:down down up
分析:我们自己拿出一个纸条来折叠,发现一个规律,每个新折痕都是在上一个旧折痕上下形成的,通常是上面凹下面凸
可以看成一个二叉树,中序遍历后就是折痕的顺序
1 | //i——当前层数 N——总共层数 |
6.图
图的存储方式
图的存储方式有很多种,如邻接表、邻接矩阵等等,这里介绍一种算法中通用的图的表示结构。
Graph:
1 | public class Graph { |
Node:
1 | public class Node { |
Edge:
1 | public class Edge { |
我们做题时只要按照我们自己定义的图的方式把题做出来,再写一个适配器把题目中图的结构转化为自己定义的结构就可以了。
如:二维数组:n*3 每行第一个元素代表权重,第二个代表出节点,第三个代表入节点。将这个结构转化为我们的图结构。
1 | public static Graph createGraph(Integer[][] matrix){ |
宽度优先遍历和深度优先遍历
宽度优先遍历:
1,利用队列实现
2,从源节点开始依次按照宽度进队列,然后弹出
3,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
4,直到队列变空
1 | public static void bfs(Node node){ |
深度优先遍历:
1,利用栈实现
2,从源节点开始把节点按照深度放入栈,然后弹出
3,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4,直到栈变空
1 | public static void dfs(Node node){ |
拓扑排序算法
适用范围:要求有向图,且有入度为0的节点,且没有环
1 | //方法:找出入度为0的节点返回,并将他的影响消除,循环往复 |
kruskal算法
适用范围:要求无向图
思想:将边按照从小到大顺序排好,按照顺序依次加边进图,每次都判断是否构成环
判断成环方法:把每个节点想象成一个单独的集合,每次边加进去符合要求的话将集合合并,如果加入边的入和出节点在一个集合中,则构成环。
1 | public static class MySets{ |
Prim算法
思想:任意找一个点存入集合,选中它的边中权重最小的一条对应的节点存入集合中,接着循环往复。如果选中的边对应的节点已放入集合中,则跳过。
1 | public static class EdgeComparator implements Comparator<Edge> { |
Dijkstra算法
适用范围:没有权值相和为负数的环
Dijkstra是求图上某个点到其他点的最短距离
1 | public static HashMap<Node,Integer> dijkstra(Node head){ |
7.前缀树和贪心算法
前缀树
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
它有3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,我们构建的树就是如下图这样的:
1 | public static class TrieNode{ |
贪心算法
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。
1 | public static class Program{ |
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。
一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60。金条要分成10,20,30三个部分。如果先把长度60的金条分成10和50,花费60;再把长度50的金条分成20和30,花费50;一共花费110铜板。但是如果先把长度60的金条分成30和30,花费60;再把长度30金条分成10和20,花费30;一共花费90铜板。输入一个数组,返回分割的最小代价。
很显然,我们用贪心策略就是先分大的,然后再分,反过来也就是从小的开始加,类似于Huffuman编码向上加到最大形成最短
1 | public static int lessMoney(int[] arr){ |
costs[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
k表示你只能串行的最多做k个项目
m表示你初始的资金
说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。
输出:你最后获得的最大钱数。
1 | public static class Node{ |
一个数据流中,随时可以取得中位数
解题方法:创建一个大根堆一个小根堆,默认第一个元素放入大根堆,接下来元素做如下步骤:
(1)判断cur<=大堆顶,如果是,放入大根堆,不是放入小根堆
(2)大根堆和小根堆的size差距等于2的时候,让size较大的堆顶弹出元素放入size较小的堆
1 | public static class MaxheapComparator implements Comparator<Integer>{ |
8.暴力递归
N皇后问题
皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数n,返回n皇后的摆法有多少种。
n=1,返回1。n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。n=8,返回92。
1 | public static int num1(int n){ |
利用位运算优化:N皇后问题时间复杂度永远是O(N!),但是我们可以通过位运算来简便常数时间。
1 | public static int num2(int n){ |
编写一个测试类看看位运算对常数级别优化了多少
1 | public static void main(String[] args) { |
暴力递归解释
暴力递归就是尝试
1,把问题转化为规模缩小了的同类问题的子问题
2,有明确的不需要继续进行递归的条件(basecase)
3,有当得到了子问题的结果之后的决策过程
4,不记录每一个子问题的解
汉诺塔问题
打印n层汉诺塔从最左边移动到最右边的全部过程
1 | public static void process(int i,String from,String to,String other){ |
字符串相关问题
打印一个字符串的全部子序列,包括空字符串
1 | public static void printAllSubsquences(String str){ |
打印一个字符串的全部排列,要求不要出现重复的排列
1 | public static ArrayList<String> printAllPermutation(String str){ |
纸牌游戏
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]。开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A…如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A…玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。
arr=[1,100,2]。开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。
1 | public static int win(int[] arr) { |
逆序栈
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。
1 | public static void reverseStack(Stack<Integer> stack){ |
字母转化
规定1和A对应、2和B对应、3和C对应…那么一个数字字符串比如”111”,就可以转化为”AAA”、”KA”和”AK”。
给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
1 | public static int number(String str) { |
背包问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
1 | public static int maxValue(int[] weight,int[] value,int bag){ |