文章目录
- 前言
- 相关题目
- 如何从链表中删除重复元素?
- 找出单链表倒数第k个元素?
- 如何实现链表反转?
- 如何从尾到头输出单链表?
- 如何寻找单链表的中间节点?
- 如何检测链表中是否有环?
- 若单链表有环,如何查找环入口?
- 如何判断不知道头指针情况下删除指定节点?
- 如何判断两个链表是否相交?
- 链表相交如何找到相交的第一个节点?
- 如何O(1)时间复制度求栈中最小元素?
- 如何两个栈模拟队列?
- 如何两个队列模拟栈?
- 如何进行选择排序
- 如何进行插入排序
- 如何进行冒泡排序
- 如何进行归并排序
- 如何进行快速排序
- 如何进行希尔排序
- 如何进行堆排序
- 各种排序算法优劣
- 如何用移位操作实现乘法运算
- 如何判断一个数是否为2的n次方
- 寻找数组中最小值与最大值
- 寻找数组中第二大的数
- 如何求最大子数组之和
- 如何找出数组中重复元素最多的数
- 如何求数组中两两相加等于20的组合种数
- 如何把一个数组循环右移K位
- 如何找出数组中第k个最小的数
- 如何找出数组中只出现一次的数字
- 如何找出数组中唯一重复的元素(每个元素只能访问一次,不用辅助存储空间)
- 如何用递归方法求一个整数数组的最大元素
- 如何求数对之差的最大值
- 如何求绝对值最小的数
- 如何求数组中两个元素的最小距离(两个元素可能重复出现)
- 如何求指定数字在数组中第一次出现的位置
- 如何对数组两个有序段进行合并
- 如何计算两个有序整型数组的交集
- 如何判断一个数组中数值是否连续相邻
- 如何求解数组中反序对个数
- 如何求解最小三元组距离
- 如何实现字符串反转
- 如何判断两个字符串是否由相同字符组成
- 如果统计一行字符中有多少个单词
- 如何按要求打印数组的排列情况
- 如何输出字符串所有组合
- 二叉树
- 如何实现二叉排序树
- 如何层序遍历二叉树
- 如何求二叉树中结点的最大距离
- 如何消除嵌套括号
- 如何不用比较运算就可以求出两数最大与最小
- 结语
前言
本次博文对何昊出版的《java程序员面试宝典》的第8章数据结构部分的概括笔记,删除其中部分知识点,保留一部分代码(而且是核心代码),大部分代码不留存,只描述思想,本次部分包含大部分的面试题。
相关题目
如何从链表中删除重复元素?
方法1:
遍历链表,把遍历值存储到HashTable中,遍历过程中访问的值在HashTable中存在,说明是重复的,可删除。
方法2:
双重循环,外循环正常遍历,外循环当前cur,内循环从cur开始遍历,与cur相同的则删除。
找出单链表倒数第k个元素?
设置两个指针,其中一个指针比另一个指针先前移k-1步,两个同时往前移,先走的到尾,后走的就是要找的位置。
如何实现链表反转?
三个指针,pNode是当前处理的节点,pre是已处理前面已反转的链表的头(初始化为null),pNext是pNode的下一个节点。pNode->next = pre;pre = pNode;pNode = pNext;当pNext为null说明pNode是新反转的头。
如何从尾到头输出单链表?
方法1:
从头到尾遍历链表,进过的节点保存到栈中,遍历完毕,从栈顶一个个弹出节点值即可。
方法2:
递归,每访问到的节点,先递归输出其后面的节点再输出自己。
如何寻找单链表的中间节点?
方法1:
先求单链表长度length,如何遍历length/2即可。
方法2:
若是双向链表,两个指针,一个从头到尾遍历,一个从尾到头遍历,指针项羽就是中间结果。
方法3:
若是单链表,用两个指针从头遍历,一个快指针一次走两步,一个慢指针一次走一步。快指针先到链表尾部,而慢指针则恰好到链表中间。(若长度为奇数,慢指针指向的便是链表中间,若是偶数,慢指针指向的节点和其下一个节点都是链表中间节点)。
如何检测链表中是否有环?
两个指针,fast是快指针,slow是慢指针,初始化都是指向头,fast每次前进两步,两个指针都向前移动,快指针每移动一次都要和满指针比较,直到块指针等于慢指针为止,就证明链表是带环的单向链表,否则是不带环(fast先达到尾部为NULL)
若单链表有环,如何查找环入口?
fast与slow相遇时,slow必定没有遍历完链表,而fast在环内循环了n圈(n>=1).
假设slow走了s步,则fast指针走了2s步(fast走的步数是s+加上多转的n圈),设环长为r。
2 s = s + n r 2s = s+nr 2s=s+nr
s = n r s = nr s=nr
设整个链表长L,入口环与相遇点距离x,起点到入口点距离为a。则
a + x = n r a+x=nr a+x=nr
a + x = ( n − 1 ) r + r = ( n − 1 ) r + L − a a+x = (n-1)r+r=(n-1)r+L-a a+x=(n−1)r+r=(n−1)r+L−a
a = ( n − 1 ) r + r = ( n − 1 ) r + ( L − a − x ) a= (n-1)r+r=(n-1)r+(L-a-x) a=(n−1)r+r=(n−1)r+(L−a−x)
L − a − x L-a-x L−a−x为相遇点到环入口点的距离,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是在链表头与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一个点为环路口点。
如何判断不知道头指针情况下删除指定节点?
分情况:
1)删除点为尾节点,无法删除,删除后无法使其前驱节点的next指针为空
2)删除点不上尾节点,则可以通过交换这个节点与其后继节点的值,然后删除后继节点
如何判断两个链表是否相交?
两个相交则必有相同尾节点,所以遍历两个链表记录它们的尾节点,若相同则相交。
链表相交如何找到相交的第一个节点?
分别计算两个链表长度len1,len2。假设len1>len2。所以head1遍历(len1-len2)个节点到p,节点p和head2到相交节点的距离相同,此时同时遍历两个链表直到遇到相同节点。
如何O(1)时间复制度求栈中最小元素?
两个栈实现,一个用来存输入A,一个用来保存栈最小元素B。
如果当前入A栈元素比B栈栈顶最小值还小,则压入B栈;出栈时,如果恰好等于当前栈最小值,则B也出栈。
如何两个栈模拟队列?
A是插入栈,B是弹出栈。
A提供入队列,B提供出队列。
入队列时,压入栈A,出队列时,B不为空,则弹B栈顶,为空,依次弹出栈A元素到栈B,再弹出B的栈顶。
如何两个队列模拟栈?
q1是入队列,q2出队列。q1提供压栈,q2提供弹栈
压栈,则入队列q1。弹栈
(1)若队列q1只有一个元素,则q1中元素出队列。
(2)不止一个元素,则所有元素出队列到q2,最后一个元素不入队列q2。输出,然后把q2所有元素入对列q1。
如何进行选择排序
给定一组记录,第一轮比较得到的最小记录,与第一个记录位置交换;接着对第一个记录以外的其他记录进行第二轮比较,得到最小记录与第二个记录位置交换;重复操作到最后进行比较记录只有一个为止。
如何进行插入排序
给定的一组记录,初始时假设第一个记录自成有序记录,其余记录为无序序列。从第二个记录开始,按照记录的大小,一次将当前处理的记录插入到其之前的有序序列中,直到最后一个记录插入到有序序列中为止。
如何进行冒泡排序
对于给定n个记录,从第一个记录开始依次对相邻的两个记录进行比较,当前面的记录大于后面记录时,交换位置。进行一轮比较和换位后,n个记录的最大值位于第n位,然后对前面n-1个记录进行第二轮进行比较,直到比较记录只剩下一个。
如何进行归并排序
利用递归与分治技术把数据序列划分为越来越小的半子表,再对半子表排序。然后再用递归方法把排序好的半子表合并为越来越大的有序序列。
对于给定的一组记录(假设n个),首先将两个相邻长度为1的子序列进行归并,得到n/2(向上取整)个长度为2或1的有序子序列,再将其两两归并,反复次过程,直到得到一个有序序列。
二路归并排序过程需要进行logn趟,每一趟归并排序的操作就是将两个有序子序列进行归并,每一对有序子序列归并时,记录的比较次数均小于等于记录的移动次数,记录移动次数均等于文件中记录的个数n,即每一趟归并的时间复杂度为 O ( n ) \mathcal{O}(n) O(n)。因此,二路归并排序的时间复杂度为 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)
如何进行快速排序
采用分而治之思想,把大的拆分为小的,小的拆分为更小的。
对于一组给定的记录,通过一趟排序后,把原序列分为两部分,其中前一部分的所有记录均比后一部分的所有记录小,然后再依次对前后两部分的记录进行快速排序,递归该过程,直到序列中所有记录均有序为止。
算法步骤:
1)分解。把输入的序列array[m…n]分为两个非空子序列array[m…k]和array[k+1…n],使得array[m…k]中任一元素的值都不大于array[k+1…n]中任一元素的值。
2)递归求解。通过递归调用快速排序分布对array[m…k]和array[k+1…n]进行排序。
3)合并。由于对分解出的两个子序列的排序就是就地进行的,使用array[m…k]和array[k+1…n]都排好序后不需要执行任何计算array[m…n]就已经排好序。
quickSort(int array[]){
sort(array,0,array.length-1);
}
sort(int array[],int low,int high){
int i,j
int index;
if(low >= high)return;
i = low;
j = high;
index = array[i];
while(i<j){
while(i<j&&array[j] >=index) j--;
if(i<j)
array[i++] = array[j];
while(i<j && array[i] < index)
i++;
if(i<j)
array[j--] = array[i];
}
array[i] = index;
sort(array,low,i-1);
sort(array,i+1,high);
}
快速排序特点
1)最坏时间复杂度。最坏情况每次区分划分结果都是基准关键字的左边(或右边)序列为空,而另一边区间中的记录项仅比排序前少一项,即选择的基准关键字是待排序的所有记录中最小或者最大的。这种情况下,需要进行(n-1)次区间划分,对于第k(0<k<n)次区间划分,划分前序列长度(n-k+1),需要进行(n-k)次记录的比较。因此当k从1到(n-1)时,进行比较次数总共n(n-1)/2。所有最坏情况快速排序时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2)
2) 最好时间复杂度。最好情况指每次区间划分结果都是基准关键字左右两边序列长度相等或相差为1,即选择的基准关键字为待排序记录中的中间值。此时进行的比较次数总共为nlogn,所以最好情况下是 O ( n l o g ( n ) ) \mathcal{O}(nlog(n)) O(nlog(n))
3) 平均时间复杂度。快排排序的平均时间复杂度为 O ( n l o g ( n ) ) \mathcal{O}(nlog(n)) O(nlog(n))。虽然快排在最坏情况下复杂度是 O ( n 2 ) \mathcal{O}(n^2) O(n2),但所有平均时间复杂度为 O ( n l o g ( n ) ) \mathcal{O}(nlog(n)) O(nlog(n))的算法中,快排平均性能是最好的。
4)空间复杂度。快排排序的过程中需要一个栈空间来实现递归。每次对区间划分都比较均匀(最好情况),递归树最大深度 ⌈ l o g n ⌉ + 1 \left \lceil log n\right \rceil +1 ⌈logn⌉+1。每次区间划分使得有一边的序列长度为0(即最坏情况),递归树最大深度为n。在每轮排序结果后比较基准基准关键字左右的记录个数,对记录多的一边先进行排序,此时栈的最大深度可降为log n。因此,快排平均空间复杂度为O(log n)。
5)基准关键字选取。该方面决定快排性能。
- 三者取中。指在当前序列中,将其首,尾和中间位置记录进行比较,选择三者中值作为基准关键字,在划分开始前,交换序列第一个记录和基准关键字的位置。
- 取随机数。取left和right之间随机m,用n[m]做基准关键字,该方法使得n[left]和n[right]之间的记录是随机分布的,采取该方法的快排称为随机快排。
快排和归并排序的不同在哪里?
快排和归并排序都是基于分而治之,首先待排序分两组,对两组分别排序最后合并结果。
不同是在进行分组策略,合并策略也不同。归并排序分组策略假设排序元素存放在数组中,那么其把前面一半元素作为一组,后面一半作为另外一组。而快速排序按照元素的值来分组,大于某个值的元素放在一组,而小于的防御另外一组,这个值称为基准。使用对快排来说,基准值挑选十分重要,如果选择不合适,太大或太小,所有元素都分在一个组。
总的来说,快排和归并,如果分组越简单,后面合并策略就越复杂,因为快排在分组已经根据元素大小来分组,而合并只需要把两个分组合并起来就成,归并则需要对两个有序数组根据大小合并。
如何进行希尔排序
也被称为“缩小增量排序”,基本原理:先将待排序的数组元素分成多个子序列,使得每个子序列元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序序列“基本有序后”,最后再对所有元素进行一次直接插入排序。
具体步骤:
1)选择一个步长序列 t 1 , t 2 , . . . , t k t_1,t_2,...,t_k t1,t2,...,tk,满足 t i > t j ( i < j ) , t k = 1 t_i>t_j(i<j),t_k=1 ti>tj(i<j),tk=1
2) 设步长序列序列个数为k,对待排序序列进行k趟排序。
3)每趟排序,根据对应的步长 t i t_i ti,将待排序列分割成 t i t_i ti个子序列,分布对各个子序列进行直接插入排序。
注意步长因子为1时,所有元素作为一个序列来处理,其长度为n。
shellSort(int array[]){
int length = array.length;
int i,j;
int h;
int temp;
for(h=length/2;h>0;h=h/2){
for(i = j;i<length;i++){
temp = array[i];
for(j=i-h;j>=0;j-=h){
if(temp < array[j])
array[j+h]=array[j]
else
break;
}
array[j+h] = temp;
}
}
}
希尔排序的关键并不是随便的分组后各自排序,而是将间隔某个“增量的记录组成一个个子序列,实现跳跃式移动,使得排序效率变高。
如何进行堆排序
堆是一种特殊的树形数据结构,其每个结点都有一个值,通常提到的堆都是指一棵完全二叉树,根节点的值小于(或小于)两个子结点的值,同时,根结点的两个子树也分别是一个堆。
堆排序是一树形选择排序,在排序过程中将R[1…n]看做一颗完全二叉树的顺序存储结构,利用完全二叉树中父节点和子节点之间内在关系来选择最小元素。
堆一般分大顶堆和小顶堆两种。
对于给定的n个记录的序列(r(1),r(2),…,r(n)),当且仅当满足条件(r(i)>=r(2i),i=1,2,…,n)时称为大顶堆。此时堆顶元素必为最大值。
对于给定的n个记录(r(1),r(2),…,r(n)),当且仅当满足条件(r(i)<=r(2i),i=1,2,…,n)时称为x小顶堆。此时堆顶元素必为最大值。
堆排序思想,对于给定的n个记录,初始时把这些记录看做一颗顺序存储二叉树,然后调整其为一个大顶堆,然后将堆的最后一个元素与堆顶元素(即二叉树的根结点)进行交换后,堆的最后一个元素即为最大元素;接着将堆的最后一个元素与堆顶元素(即二叉树的根结点)进行交换后,堆的最后一个元素即为最大记录;接着将前(n-1)个元素(即不包括最大记录)重新调整为一个大顶堆,再将堆顶元素与当前堆的最后一个元素进行交换后得到。
堆排序的两个过程:一个构建堆,第二个是交换堆顶与最后一个元素位置。
adjust(int []a,int pos,int len){
int temp;
int child;
for(temp=a[pos];2*pos+1<=len;pos=child){
child = 2*pos+1;
if(child<len && a[child] > a[child+1])
child++;
if(a[child] < temp)
a[pos]=a[child]
else
break;
}
a[pos]=temp;
}
myMinHeapSory(int []array){
int i;
int length = array.length;
for(i = len/2-1;i>=0;i--)
adjustMeap(array,i,len-1);
for(i=len-1;i>=0;i--){
int temp = array[0];
array[0] = array[i];
array[i] = temp;
adjustMinHeap(array,0,i-1);
}
}
对排序对记录较少文件效果一般,对记录较多文件很有效。主要耗时在创建堆和反复调整堆上。最坏情况下,时间复杂度也为 O ( n ∗ l o g ( n ) ) \mathcal{O}(n*log(n)) O(n∗log(n))
各种排序算法优劣
排序算法 | 最好时间 | 平均时间 | 最坏时间 | 辅助存储 | 稳定性 | 备注 |
---|---|---|---|---|---|---|
简单选择排序 | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( 1 ) \mathcal{O}(1) O(1) | 不稳定 | n小时较好 |
直接插入排序 | O ( n ) \mathcal{O}(n) O(n) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( 1 ) \mathcal{O}(1) O(1) | 稳定 | |
冒泡排序 | O ( n ) \mathcal{O(n)} O(n) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( 1 ) \mathcal{O}(1) O(1) | 稳定 | |
希尔排序 | O ( n ) \mathcal{O(n)} O(n) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n s ) 1 < s < 2 \mathcal{O}(n^s)1<s<2 O(ns)1<s<2 | O ( 1 ) \mathcal{O}(1) O(1) | 不稳定 | |
快速排序 | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n 2 ) \mathcal{O}(n^2) O(n2) | O ( l o g ( n ) ) \mathcal{O(log(n))} O(log(n)) | 不稳定 | |
堆排序 | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( 1 ) \mathcal{O}(1) O(1) | 不稳定 | |
归并排序 | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n)) | O ( n ) \mathcal{O}(n) O(n) | 稳定 |
结论:
- 所有数经过某种排序方法后,仍能保持它们在排序之前的相对次数,就称这种排序方法是稳定的。反之是不稳定的。
- 时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2)有直接插入排序,冒泡排序,归并排序,时间复杂度为 O ( n l o g ( n ) ) \mathcal{O(n log(n))} O(nlog(n))有希尔排序,快速排序,简单选择排序和堆排序。
- 空间复杂度为 O ( 1 ) \mathcal{O}(1) O(1)的有简单选择排序,直接插入排序,冒泡排序,希尔排序和堆排序。空间复杂度 O ( n ) \mathcal{O}(n) O(n)有归并排序, O ( l o g ( n ) ) \mathcal{O(log(n))} O(log(n))的有快速排序。
- 虽然直接插入排序和冒泡排序速度比较慢,但当初始序列整体或局部有序,这两种排序有较高效率,快排效率则下降。当排序序列较小不要求稳定,直接选择排序较好;要求稳定性,冒泡排序较好。
除了上述排序,还有其他排序,位图排序,桶排序,基数排序等。每个排序都有最佳使用场景。
如何用移位操作实现乘法运算
把一个数向左移位n位相当于把该数乘以2的n次方。因此当乘法运算中某个数字满足这个特点时,就可以移位操作来代替乘法操作,从而提高效率。
如何判断一个数是否为2的n次方
2的n次方可以表示为 2 0 , 2 1 , 2 2 , . . . , 2 n 2^0,2^1,2^2,...,2^n 20,21,22,...,2n。
方法一:
用1做移位操作,然后判断位移后的值是否与给定的数相等。时间复杂度 O ( l o g ( n ) ) \mathcal{O}(log (n)) O(log(n))
方法二:
考虑某数是2的n次方,这个数对应的二进制表示只有一位1,其余都为0,所以只需判断这个数二进制表示是否只有一位1。再优化一下,假设一个num = 00010000,那么num-1为00001111,由于num与num-1二进制每一位表示都不相同,所以两者相与运算结果为0,用这种方法来判断一个数是否为2的n次方。
如何求二进制数中1的个数
方法一:
判断数字最后一位是否是1,为1计数器加一。然后右移抛弃最后一位。循环执行该操作直到这个数等于0为止,采用位运算来达到该目的,时间复杂度 O ( n ) , n 为 二 进 制 位 数 \mathcal{O}(n),n为二进制位数 O(n),n为二进制位数。
while(n>0){
if((n&1)== 1)count++;
n>>=1;
}
方法二:
要求1的个数,可以把二进制每个1看成独立个体,利用n和(n-1)结果中都会少一位1,而且是最后一位。时间复杂度为 O ( m ) , m 是 二 进 制 数 1 的 个 数 \mathcal{O}(m),m是二进制数1的个数 O(m),m是二进制数1的个数。
while(n>0){
if(n!=0){
n = n&(n-1);
count++;
}
}
寻找数组中最小值与最大值
- 问题分解法;看成两个独立问题,每次分别寻找出最大值和最大值,需要遍历两次数组,比较次数为2N(N是数组大小)次。
- 取单元素法。维持变量min和max,每次取出元素,与找到的最小值比较和最大值比较并更新,只需遍历一次。
- 取双元素法。维持min和max,每次比较相邻两数,较大与max比较,较小与min,比较次数1.5N。
- 数组元素移位法。将数组中相邻两个数分在一组,每次比较两个相邻的数,将较大交换到这两个数左边,较小在右边。对大着扫描一次最大值,对小者组扫描一次,需要比较1.5~2N次,但需要改变数组结构。
- 分治法。将数组分两半,分别找出两边最大值和最小值,则最大值和最小值分别是两边最小值的较小值,两边最大值的较大者。该法需要比较1.5N次。
寻找数组中第二大的数
1)用排序将数组排序,根据下标访问数组第二大的数,用快排,但时间复杂度 O ( n log n ) \mathcal{O}(n\log n) O(nlogn),根据下标问需要遍历一遍数组,时间复杂度为 O ( n ) \mathcal{O}(n) O(n),所以总时间复杂度为 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)
2) 设置两个变量,一个用来存储最大值,初始化为元素首元素,另一个变量用来存储数组元素的第二大叔值,初始化为最小负整数,然后遍历数组元素。如果数组元素值比最大数变量的值打,则将第二大变量的值更新为最大值变量的值,最大值变量的值更新为该数组元素的值。如果数组元素的值比最大值小,则判断是否比第二大数的值大,若大则更新第二大数的值为该数组元素的值。
如何求最大子数组之和
方法一:蛮力法。
找出所有子数组,然后求出子数组的和,所有子数组的和中取最大值。时间复杂度 O ( n 3 ) \mathcal{O}(n^3) O(n3),效率低,且许多子数组重复计算。
方法二:
Sum[i,j]=Sum[i,j-1]+arr[j],可以省去计算Sum[i,j-1],提高程序效率。 O ( n 2 ) \mathcal{O}(n^2) O(n2)
方法三:动态规划
首先根据数组最后一个元素arr[n-1]与最大子数组关系分三种情况:
(1)最大子数组包含arr[n-1],即arr[n-1]
(2)arr[n-1]单独构成最大子数组
(3)最大子数组不包含arr[n-1],那么求arr[1,…,n-1]的最大子数组可以转换为求arr[1,…,n-2]的最大子数组。
假设已经计算出(arr[0],…,arr[i-1])最大的一段数组和为All[i-1],同时也计算出该包含arr[i-1]的最大一段数组和为End[i-1],则可以得出关系,All[i-1] = max{arr[i-1],End[i-1],All[i-2]}。
时间复杂度 O ( n ) \mathcal{O}(n) O(n),空间复杂度 O ( n ) \mathcal{O}(n) O(n)
方法四:优化的动态规划
每次都用到End[i-1]与All[i-1],而不是整个数组中的值,因此可以定义两个变量保存End[i-1]和All[i-1]的值,且反复利用。时间复杂度 O ( n ) \mathcal{O}(n) O(n),空间复杂度 O ( 1 ) \mathcal{O}(1) O(1)
for(int i=1;i<n;++i){
nEnd =max(nEnd+arr[i],arr[i]);
nAll = max(nEnd,nAll);
}
return nAll;
如何找出数组中重复元素最多的数
方法一
定义int count[Max],都为0。然后count[A[i]]++;从count数组找出最大数,即为重复次数最多的数。(除非内存够大,否则不采用)
方法二
使用Map映射表记录每个元素出现次数,然后判断次数大小,找到重复次数最多的元素。
如何求数组中两两相加等于20的组合种数
方法一:蛮力
两重循环,判断两个数是否为20。 O ( n 2 ) \mathcal{O}(n^2) O(n2)
方法二:排序法
先排序(快排或堆排序),复杂度 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)。然后对排序后数组分别从前导后和从后到前偏离,假设从前往后遍历的下标为begin,从后往前的下标为end,当arr[begin]+arr[end]<20时,如果存在两个数的和为20,则一定在[begin+1,end]之间。arr[begin]+arr[end]>20时,如果存在两个数和为20,那么一定在[bein,end-1]之间。这个过程是 O ( n ) \mathcal{O}(n) O(n),所以总 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)。
如何把一个数组循环右移K位
比较前后数组序列形式,由于两段序列顺序不变,可以将这两段看作两个整体,右移K位就是把数组两部分交换一下。
12345678->78123456
(1)逆序子序列123456,结果逆序65432178
(2)逆序数组子序列78,结果逆序65432187
(3)完全逆序,变成78123456
shift_k(int a[],int k){
int n = a.length;
k = k% n;
reverse(a,n-k,n-1);
reverse(a,0,n-k-1);
reverse(a,0,n-1);
}
reverse(int a[],int b,int e){
for(;b<e;b++,e--){
int temp = a[e];
a[e] = a[b];
a[b] = temp;
}
}
如何找出数组中第k个最小的数
方法一:排序
对排序后的数组找第k-1个位置上的数字即为数组的第k个最小的数。 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)
方法二:“剪枝法”
采用快排思想,选一个数tmp=a[n-1]作为枢纽,把比它小的数放左边,让他大的数都放在右边,判断tmp的位置,如果位置是k-1,则是第k个最小的数。如果位置小于k-1,则第k个小元素一定在数组右半部分,采用递归方法在数组右半部分继续查找,否则第k个小元素在数组左半部分,递归左半查找。
如何找出数组中只出现一次的数字
最简单排序后第一个数字遍历,从而找出, O ( n log n ) \mathcal{O}(n\log n) O(nlogn)
如果除了这个数字其他都出现了2次。
异或运算,每个数字异或自己都等于0,,从头到尾异或数组中每个数字,出现两次全部都在异或中被抵消,最终结果刚好是这个出现一次的数字。
如果除了这个数字其他都出现了3次。
假设出现一次的数字为a,去掉a后所有数字对于的二进制的每个位置出现1的个数都为3的倍数,因此可以找出这个数。
int n = a.length;
int []bitCount = new int [32];
for(int i=0;i<n;i++)
for(int j=0;j<32;j++)
bitCount[j] +=((a[i]>>j)&1);
int appearOne = 0;
for(int i=0;i<32;i++)
if(bigCount[i] % appearTimes != 0)
appearOne +=(1<<i);
return appearOne;
如何找出数组中唯一重复的元素(每个元素只能访问一次,不用辅助存储空间)
数组a[N],把1~N-1存放在a[N],其中某数重复1次。
(1)数学求和法,因为只有一个数字重复1次,又是连续的,根据累加和原理,对数组所有项求和,减去1~N-1的和,即为所求的重复数。
如果没有要求只能访问一次而且不允许用辅助空间则可以使用异或或位图求解。
(2)异或法
每两个相异的数执行异或之后运算为1,没两个相同的数执行异或之后结果为0,使用,数据a[N]中的N个数异或结果与1~N-1异或的结果再做异或,得到的值即为所求。
设重复数为A,剩余N-2个数异或的结果为B,N个数异或结果为A^A^B,1~n-1异或结果为A^B,由±于异或满足交换律和结合律,,且X^X=0,0^X = X,则(A^B)^(A^A^B)=A^B^B = A。
for_findDup(int []a){
int n=a.length;
int i;
int result=0;
for(i=0;i<n;i++)result^=a[i];
for(i = 1;i<n;i++)result^=i;
return result;
}
(3)空间换时间
申请长度为N-1的整型数据flag并初始化为0,然后从头开始遍历数组,取每个数组元素a[i]的值,将其对应的数组flag中元素赋值为1,如果已经置过1,那么该数就是重复的数。
也可以用位图方法降低空间复杂度,而不用整型,而是用1bit表示。需要申请数组长度为N/32,取上整。
取值为[1,n-1]含n个元素的整数数组,至少存在一个重复数,即可能存在多个重复数, O ( n ) \mathcal{O}(n) O(n)时间内找出其中任意一个重复数
方案1:位图。使用大小为n的位图,记录每个元素是否已经出现过,一旦已经出现过,则输出。时间和空间复杂度都是 O ( n ) \mathcal{O}(n) O(n)。
方法2:排序。对数组进行计数排序,然后顺序扫描整个数组,一旦遇到一个已出现元素,则直接输出。时间和空间复杂度都是 O ( n ) \mathcal{O}(n) O(n)。
方法3:取反法。如果遍历到数组中的元素为i,那么把a[i]值取反,如果i在数组中出现,那么a[i]会进过两次取反操作,a[i]的值跟原始的值相等,且为正数。如果i出现1次,那么a[i]的值为原始值的相反数,且为负数。
实现方法:将数组元素作为索引,对于元素array[i],如果array[array[i]]大于0,那么设置a[array[i]]=array[array[i]];反之,则设置arraya[-array[i]]=-array[-array[i]],最后从数组第二个元素开始遍历数组,最后从数组第二个元素开始遍历数组,如果array[i]>0,那么这个数就是重复的。由于在进写遍历后对数组中数据进写修改,因此需要对数据进行还原(对数组中负数取反)。
sor_findDup(int []a){
int n = a.length;
int result = Integer.MAX_VALUE;
for(int i=0;i<n;i++){
if(a[i]>0)
a[a[i]]=-a[a[i]];
else
a[-a[i]]=-a[-a[i]];
}
for(int i=1;i<n;i++){
if(a[i]>0)
result = i;
else
a[i]=-a[i];
}
return result;
}
方法四:类似“已知单链表存在环,找出环入口”
具体:将array[i]看做第i个元素的索引,即array[i]->array[array[i]]->array[array[array[i]]]->array[array[array[array[i]]]]->
最终形成一个单链表,由于数组a中存在重复元素,因此一定存在一个环,且环的入口元素即为重复元素。
关键在于,数组长度为n,而元素范围是[1,n-1],所以array[0]不会指向自己,进而不会陷入错误的自循环。如果元素范围中包含0,则不可以直接采用此法。
findInteger(int a[]){
int x=0,y=0;
do{
x = a[a[x]];// x一次走两步
y = a[y];// y一次走一步
}while(x!=y)//找到环中一点
x = 0;
do{
x=a[x];
y=a[y];
}while(x!=y)
return x;
}
如何用递归方法求一个整数数组的最大元素
递归的求解“数组第一个元素”与“数组中其他元素组成的子数组的最大值”的最大值。
maxnum(int a[],int begin){
if(a.length-begin == 1)
return a[begin]
else
return max(a[begin],maxnum(a,begin+1));
}
如何求数对之差的最大值
描述:数组中一个数字减去它右边子数组中的一个数字可以得到一个差值,求所有可能的差值中最大值。
{1,4,17,3,2,9},17-2=15
方法一:“蛮力”法
针对数组a中每个元素a[i](0<i<n-1),求所有a[i]-a[j](i<j<n)的值中最大值。
方法二:二分法
把数组分为两个子数组;那么最大差值有三种可能
(1)最大的差值对应的被减数和减数都在左子数组中,假设是最大差值leftMax;
(2)被减数和减数都在右子数组中,假设是最大差值rightMax;
(3)被减数是左子数组最大值,减数是右子数组最小值,假设差值mixMax
然后这个数组最大差值就是这3个差值的最大值,即max(leftMax,rightMax,mixMax)
仅一次遍历,时间复杂度 O ( n ) \mathcal{O}(n) O(n)。
方法三:动态规划
给定数组a,申请额外数组diff和max,其中diff[i]是以数组中第i个数字为减数的所有数对之差的最大值(前 i+1个数组成的子数组中最大的差值),max[i]为前i+1个数的最大值。假设已经求得diff[i],diff[i+1]的值有两种可能性:
(1)等于diff[i];
(2)等于max[i]-a[i]
动态规划方法表达:
diff[i+1] = max(diff[i],max[i-1]-a[i])
max[i+1]=max(max[i].a[i+1])
数组最大差值为diff[n-1],n是数组长度
时间复杂度和空间复杂度都是 O ( n ) \mathcal{O}(n) O(n)。
可以优化,可以看出,求解diff[i+1]只用到diff[i]与max[i],而与数组diff和max中其他数字武馆,可以通过两个变量而不是数组来记录diff[i]和max[i]的值,从而降低算法空间复杂度。
方法四:求最大子数组之和方法求解
给定一个数组a(数组长度为n),额外申请一个长度为n-1的数组diff,数组diff中的值满足diff[i]=a[i]-a[i+1].那么a[i]-a[j] (0<i<j<n)就等价于diff[i]+diff[i+1]+…+diff[j]。因此求所有a[i] -a[j] 组合最大值可以转换为求diff[i]+diff[i+1]+…+diff[j]组合的最大值。由于diff[i]+diff[i+1]+…+diff[j]代表diff的一个子数组,利用求最大子数组之和方法来解决。
如何求绝对值最小的数
升序排列数组,数组有正负和0,分情况
(1)如果第一个元素为非负数,那么绝对值最小的数即为数组的第一个元素。
(2)如果最后一个元素为负数,那么绝对值最小的数肯定为数组的最后一个元素
(3)数组中既有正数又有负数,首先找到正负的分界点,分界点恰好为0,则0是绝对值最小的数,否则通过比较分界点左右的正负数的绝对值确定最小的数。
方法1:顺序遍历数组,找出第一个非负数(前提数组有正有负),接着比较分界点两个数的值找出绝对值最小的数。最好情况下复杂度为 O ( n ) \mathcal{O}(n) O(n)
方法2:取数组中间位置的值a[mid]。如果a[mid]等于0,则必然是绝对值最小的数。如果a[mid]大于0,而a[mid-1]<0,则找到了分界点,比较两者绝对值得到结果。如果a[mid-1]=0,则这个数就是绝对值最小数,分批走往左半部分查找。如果a[mid]小于0,如果a[mid+1]>0,则比较两者绝对值即可。如果a[mid+1]-0,则a[mid+1]是答案。否则在数组右半部分继续查找。
如何求数组中两个元素的最小距离(两个元素可能重复出现)
遍历数组:
(1)当遇到n1,记录对应下标n1_index,通过n1_index与上次遍历到n2的下标值n2_index的差,可以求最近一次遍历的n1与n2的距离。
(2)当遇到n2,记录下标n2_index,然后求n2_index与上次遍历到n1的下标值n1_index的差,求出最近一次遍历的n1到n2的距离。
定义变量min_dist,记录n1与n2的最小距离。每次求出n1到n2的距离与min_dist相比,求出最小值。时间复杂度 O ( n ) \mathcal{O}(n) O(n)。
如何求指定数字在数组中第一次出现的位置
给定数组a=[3,4,5,6,5,6,7,8,9,8],这个数组中相邻元素之差都为1,给定数组9求出第一次出现位置(8)。
方法1:蛮力。
遍历数组每个元素,并与给定数字比较,若相等则返回下标位置,若遍历完还没找到,则给定数字不存在,返回-1。时间复杂度 O ( n ) \mathcal{O}(n) O(n)。
方法2:跳跃搜索法。
假定要找数字9,因为数组相邻差为1,假设数组第一个元素是3(下标0),相差为6,则肯定在第七个位置(0+6)位置才可能出现数字9,如果不是递增的,那么9也应该在7后面。
从数组第一个元素开始(i=0),把当前位置值与t比较,如果相等,则返回数组下标,否则从数组下标为i+|t-a[i]|处查找。
如何对数组两个有序段进行合并
a[0,mid-1],a[mid,n-1]各自有序。对a[0,n-1]两个有序段进行合并,令整体有序,要求空间复杂度为 O ( 1 ) \mathcal{O}(1) O(1)。(注意a[i]元素支持<运算)。两个都是有序段升序。
空间复杂度为 O ( 1 ) \mathcal{O}(1) O(1),不能使用归并,想到插入排序,时间复杂度 O ( n 2 ) \mathcal{O}(n^2) O(n2)。但没有用到各自有序条件。
实现:
遍历下标0~mid-1元素,将遍历元素值与a[mid]比较,当遍历到a[i] (0<=i<=mid-1) ,满足a[mid]<a[i],则两者交换。接着找到交换后的a[mid]在a[mid,num-1]中具体位置(在a[mid,num-1]进行插入排序),实现方法遍历a[mid~mid-2],如果a[mid+1]<a[mid],那么交换a[mid]与a[mid+1]的位置。
引申:
(1)如果数组两个子有序都按降序排列,也可如此解决。
(2)如果其中一个子序按升序,另一个是降序,首先针对其中一个子序列逆序,然后采用上面方法。
如何计算两个有序整型数组的交集
假设含有n个元素的有序(非降序)数组a和b,a=[0,1,2,3,4,5],b=[1,3,5,7,9],则交集[1,3].
数组交集可采用多种方法,相对算法一般会影响算法效率,所以根据两个两个数组相对大小确定采用方法。
(1)两个数组长度相当
方法1:二路归并。设两个数组array1[n1]和array2[n2],分别从i,j从头开始遍历两个数组。若array1[i]和array2[j]相等,则是交集,记录下来,继续向后遍历。若array1[i]>array2[j],需向后遍历array2。若array1[i]<array2[j],则向后遍历array1,直到遍历结束。
方法2:顺序遍历。将数组元素存放hash表,对统计数组进行计数,若为2,则是交集元素。
方法3:散列法。遍历两数组任意一个数组,将遍历得到的元素存放在散列表,然后遍历另一个数组,对建立散列表查询,若存在则为交集元素。
(2)悬殊情况,若a长度远大于b。
方法1:遍历长度短数组,将遍历得到元素在长数组进行二分查找。
设两个指向两个数组末尾元素的指针,取较小的那个数在另一个数组中二分查找,找到则存在一个交集。并将该目标数组的指针指向该位置的前一个位置。若没用找到,同样可以找到一个位置,使得目标在该位置后的数肯定不在另一个数组存在,直接移动该目标数组的指针指向该位置的前一个位置,再循环找,直到一个数组为空为止。两数组可能出现重复数,因此找到一个相同的数x,其下标为i,则下一个二分查找下界变为i+1(应该是i-1,原书很多有问题),避免x被重复使用。
方法二:在方法一上,每次在前一次查找基础进行,缩短查找表长度。
方法三:采用与方法二类似的方法,但遍历小数组方式不同,从数组头和尾同时遍历,进一步缩小。
如何判断一个数组中数值是否连续相邻
从0~65535,随机抽5个数字,判断相邻与否。0可以统配任意数字,可以多次出现,全0也算连续。只有一个非0算连续。
解决:没有0存在,最大值和最小值差距必须是4,有0情况,差距小于4即可。找出数组非0最大和非0最小,时间复杂度为 O ( n ) \mathcal{O}(n) O(n)。
如何求解数组中反序对个数
给定数组a,如果a[i]>a[j] (i<j),则是反序。
方法1:蛮力法
对数组中每一个数字,遍历其后所有数字,如果比它小,就是一个逆序对。
方法2:分冶归并法。
在归并排序基础上额外使用计数器来记录逆序对个数。
如何求解最小三元组距离
已知三个升序整数数组a[l],b[m],c[n]。三个数组各找一个元素,使得组成三元组距离最小。三元组距离定义:假设a[i],b[j],c[k]是一个三元组,那么距离为Distance=max(|a[i]-b[j]|,|a[i|-c[k]|,|b[j]-c[k]|)。
方法1:蛮力法
分别3个数组中元素,分别求出它们距离,然后从这些值查找最小值。
没有利用数组升序特性。时间复杂性 O ( l ∗ m ∗ n ) \mathcal{O}(l*m*n) O(l∗m∗n)
方法二:最小距离法
假设当前遍历的元素分布是 a i , b i , c i , 且 a i < = b i < = c i a_i,b_i,c_i,且a_i<=b_i<=c_i ai,bi,ci,且ai<=bi<=ci,则距离肯定是 D i = c i − a i D_i = c_i-a_i Di=ci−ai。
1)接下来求 a i , b i , c i + 1 a_i,b_i,c_{i+1} ai,bi,ci+1的距离,由于 c i + 1 > = c i c_{i+1}>=c_i ci+1>=ci,所以距离必定为 D i + 1 = c i + 1 − a i D_{i+1}=c_{i+1}-a_i Di+1=ci+1−ai,显然 D i + 1 ≥ D i D_{i+1}\ge D_i Di+1≥Di,所以 D i + 1 D_{i+1} Di+1不可能为最小距离。
2)接下来求 a i , b i + 1 , c i a_i,b_{i+1},c_i ai,bi+1,ci的距离,由于 b i + 1 ≥ b i b_{i+1}\ge b_i bi+1≥bi,如果 b i + 1 ≤ c i b_{i+1}\le c_i bi+1≤ci 此时他们的距离任然为 D i + 1 = c i − a i D_{i+1}=c_i-a_i Di+1=ci−ai,如果 b i + 1 > c i b_{i+1}>c_i bi+1>ci,此时它们距离为 D i + 1 = b i + 1 − a i D_{i+1}=b_{i+1}-a_i Di+1=bi+1−ai,显然 D i + 1 ≥ D i D_{i+1}\ge D_i Di+1≥Di。
3)如果接下来求 a i + 1 , b i , c i a_{i+1},b_i,c_i ai+1,bi,ci的距离,如果 a i + 1 < ∣ c i − a i ∣ + c i a_{i+1}<|c_i-a_i|+c_i ai+1<∣ci−ai∣+ci,此时它们的距离 D i + 1 = c i − a i + 1 D_{i+1}=c_i-a_{i+1} Di+1=ci−ai+1,显然 D i + 1 ≤ D i D_{i+1}\le D_i Di+1≤Di,因此 D i + 1 D_{i+1} Di+1有可能是最小距离
从3个数组的第一个元素开始,先求出它们的距离minDist,接着找出这3个数最小数对应的数组,只对整个数组的下标往后移一个位置,接着求3个数组中当前元素的距离,若比minDist小,则把当前距离幅值给minDist,一以此类推直到遍历完其中一个数组。
minDistance(int a[],int []b,int []c){
int aLen = a.length,bLen=b.length,cLen=c.length;
int curDist =0,min=0,minDist=Integer.MAX_VALUE,i=0,j=0,k=0;
while(true){
curDist = max(Math.abs(a[i]-b[j]),Math.abs(a[i]-c[k]),Math.abs(b[j]-c[k]));
if(curDist<minDist)minDist=curDist;
min=min(a[i],b[j],c[k]);
if(min==a[i])
if(++i >= aLen)break;
if(min==b[j])
if(++j >= bLen)break;
if(min==c[k])
if(++ k>= cLen)break;
}
return minDist;
}
如何实现字符串反转
两次反转,第一次对整个字符串反转,第二次对每个单词反转。
如何判断两个字符串是否由相同字符组成
指的是组成两个字符串的字母和各字母个数相同,而排列顺序不同。
方法1:排序法
对两个字符串中的字符进行排序,比较两个排序后的字符串是否相等。若相等,则是相同字符组成,否则不是。时间复杂度 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)。
方法2:空间换时间
ASCII码有266个(0~255),申请大小为266的数组记录各个字符出现的个人,并初始化为0。然后遍历第一个字符串,把字符串中字符对应ASCII码值作为作为数组下标,把对应数组元素加1,然后遍历第二个字符串,把数组中对应的元素值-1。如果最后数组各个元素值都为0,则说明两个字符串是由相同的字符组成,否则不是。时间复杂度 O ( n ) \mathcal{O}(n) O(n)。
如何删除字符串中重复字符
方法1:蛮力法
把字符串看成一个字符数组,对数组使用双重循环,如果发现重复字符,把该字符置为’\0’,最后把字符数组中的’\0’去掉,得到的字符串就是删除字符后的目标字符串。
时间复杂度 O ( n 2 ) \mathcal{O}(n^2) O(n2)。
方法2:空间换时间
常见字符256个,申请256个int类型数组记录每个字符出现次数,初始化为0,把字符编码作为数组下标,遍历数组时,如果这个字符出现为0,把它置1。如果为1,已经出现,则置为’\0’,最后去掉所有’\0’实现去重的目的。
时间复杂度 O ( n ) \mathcal{O}(n) O(n)。
改进,则申请大小为8的int类型数组,每个int占据32bit。因此大小为8数组共256bit,用1bit表示一个字符是否已经出现过达到同样目的。
方法三:正则表达式
(?s)(.)(?=.*\\1)
需要先反转字符串,再用正则表达式,然后再实现字符串反转
如果统计一行字符中有多少个单词
单词数目由空格出现次数决定(连续若干空格作为出现一次空格,一行开头的空格不统计在内)。若检测出某一个字符为非空空格,则它的前面的字符是空格,则表示“新的单词开始了”,此时使单词计数器count值加1;若当前字符为非空字符而且前面也是非空字符,则意味着仍然是原来那个单词的继续,count值不应该累加。前面一个字符是否空格,从word的值看出来,若word等于0,则表示则一个字符是空格,若word等于1,意味着前一个字符为非空格。
如何按要求打印数组的排列情况
描述:针对1,2,2,3,4,5,这6个数字,写函数打印所有不同排列。要求“4”不能第三位,“3”与“5”不能相连。
最简单的方法是递归,然而,数字中存在重复数组,第二明确规定某些位特性,常规方法不能使用。
换思路,把这6个数字排列组合问题转换成图的遍历。数字变成6个结点,6个结点两两组成个无向连通图。6个数字对应的全排列等价于从这个图的各个阶段出发深度遍历这个图所有可能路径所组成的数字集合。由于“3”与“5”不能相连,所有构造图使用图中3和5对应的结点不连通。“4”不能在第三位,可以在遍历结束后判断是否满足该条件。
具体实现:
1)用1,2,2,3,4,5这6个数字作为6个结点,构造一个无向连通图。除了“3"和”5“不连通,其他结点都两两相连。
2)分布从这6个结点出发对图做深度优先遍历,每次遍历完所有节点,把遍历路径对应数字的组合记录下来。若这个数字的第三位不是“4”,则把这个数字存放在集合Set中(避免重复)。
3)遍历Set集合打印集合所有结果。
如何输出字符串所有组合
假设字符串中字符都不重复,输出字符串所有组合。比如“abc"有”a“,”b“,”c“,”ab“,”ac“,”bc“,”abc“。
根据题意,如果有n个字符,需要输出 2 n + 1 2^n+1 2n+1种组合。
容易想到的是递归法,遍历字符串,每个字符只能取或不取。若取就把其放到结果字符串中,遍历完毕则输出字符串。
优化。考虑构造长度为n的01字符串(或二进制数)表示输出结果是否包含某字符。比如“001”表示不包含字符a,b,所以输出c。而“101"表示输出结果"ac"。所以题目要求输出”001“和”111“这 2 n − 1 2^n-1 2n−1个组合对应的字符串。
二叉树
是 n ( n ≥ 0 ) n(n\ge 0) n(n≥0)个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。当集合为空,该二叉树被称为空二叉树。
二叉树中一个元素也被称为一个节点。二叉树递归定义:二叉树或者是一颗空树,或者由一颗由一个根结点和两颗互不相交的分别称为根节点的左子树和右子树所组成的非空树,左子树和右子树又同样是一颗二叉树。
常见二叉树基本概念:
1)结点的度。结点所拥有子树的个数称为该结点的度。
2)叶结点。度为0的结点称为叶结点,或称为终端结点。
3)分枝节点。度不为0的结点称为分枝结点,或非终端结点。一颗数的结点除了叶结点外,其余都是分支结点。
4)左孩子、右孩子、双亲。树中一个结点的子树的根结点称为这个结点的孩子。这个结点称为它孩子结点的双亲。具有同一双亲的孩子结点称为兄弟。
5)祖先、子孙。树中,如果有一条路径从结点M到结点N,那么M称为N的祖先,N称为M的子孙。
6)结点层数。根结点层数为1,其他结点层数等于双亲结点的层数+1。
7)树的深度。树的所有结点的最大层数称为树的深度。
8)树的度。树中各结点度的最大值称为该树的度,叶子结点度为0.
9)满二叉树。二叉树所有分支存在左右子树,且叶结点均在同一层,被称为满二叉树。
10)完全二叉树。深度为k的n结点二叉树,从上往下,从左往右编号。该树中,编号为i的结点与满二叉树编号为i的结点在二叉树位置相同。叶子结点只能出现在最下层和次下层。最下层叶子结点集中在树的左部。满二叉树一定是完全二叉树,反之不一定。
二叉树性质
(1)非空二叉树第i层最多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 1 i\ge1 i≥1)
(2)深度为k的二叉树,最多具有 2 k − 1 2^k-1 2k−1个结点,最少有 k k k个结点。
(3)非空二叉树,度为0的结点(叶子结点)总比度为2的结点多一个。即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
对于二叉树的结点总数 n n n总有 n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2,根据二叉树和数的性质,可知道 n = n 1 + 2 ∗ n 2 + 1 n=n_1+2*n_2+1 n=n1+2∗n2+1(所有结点的度数之和+1=结点总数),结合两个等值证明该命题。
(4)具有n个结点的完全二叉树深度为 ⌊ log 2 n ⌋ + 1 \left \lfloor \log_2n \right \rfloor+1 ⌊log2n⌋+1
深度为k的二叉树最多 2 k − 1 2^k-1 2k−1,完全二叉树定义是与同深度满二叉树前面编号相同,即它的总结点数n位于k层和k-1层满二叉树容量之间。所以 2 k − 1 − 1 < n ≤ 2 k − 1 2^{k-1}-1<n\le 2^k-1 2k−1−1<n≤2k−1或 2 k − 1 ≤ n < 2 k 2^{k-1}\le n <2^k 2k−1≤n<2k,三遍同时取对数,于是有 k − 1 ≤ log 2 n < k k-1\le\log_2 n<k k−1≤log2n<k,因为k是整数,所以 k = ⌊ log 2 n ⌋ + 1 k = \left \lfloor \log_2n \right \rfloor+1 k=⌊log2n⌋+1
(5)具有n个结点完全二叉树,对于任意序号为i的结点,如果 i > 1 i>1 i>1,则双亲结点序号为 i / 2 i/2 i/2(/是整除);如果 i = 1 i=1 i=1,则其是根结点。如果 2 i ≤ n 2i\le n 2i≤n,序号为i的结点的左孩子结点的序号为2i;如果 2 i > n 2i > n 2i>n,则无左孩子。如果 2 i + 1 ≤ n 2i+1\le n 2i+1≤n,则该结点的右孩子序号 2 i + 1 2i+1 2i+1,反之无右孩子。
若根结点从0编号,则i号结点的双亲结点编号为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2,左孩子 2 i + 1 2i+1 2i+1,右孩子 2 i + 2 2i+2 2i+2
完全二叉树有1001结点,其中叶子结点个数是?
由 n = n 0 + n 1 + n 2 = n 0 + n 1 + ( n 0 − 1 ) = 2 ∗ n 0 + n 1 − 1 n=n_0+n_1+n2 = n_0+n_1+(n_0-1)=2*n_0+n1-1 n=n0+n1+n2=n0+n1+(n0−1)=2∗n0+n1−1。而完全二叉树中 n 1 n_1 n1只能取0或1。若 n 1 = 1 , n 0 = 500.5 n_1=1,n_0=500.5 n1=1,n0=500.5不符合。使用 n 1 = 0 , n 0 = 501 n_1=0,n_0=501 n1=0,n0=501。
根层次为1,具有61结点的完全二叉树的高度为多少?
2 6 − 1 < 62 < 2 6 2^{6-1}<62<2^6 26−1<62<26,所以是6层。
在具有100个结点的树,其边数目为多少?
一棵树中,除了根结点,每个结点都有一条边,总边数应是100-1,即99条。
如何实现二叉排序树
二叉排序树又称二叉查找树。具有性质:(1)左子树不空,则左树结点均小于根节点值;(2)右子树不空,则右树结点均大于根结点值。(3)左右子树均为二叉排序树。
如何层序遍历二叉树
使用队列。将根节点放入队列中,然后每次都从队列中取出节点并打印,若该节点有子节点,则子节点放入队尾,直到队列为空。
如何求二叉树中结点的最大距离
节点距离是指两个节点之间边的个数。
采用递归。求左子树距根结点最大距离,记为leftMaxDistance;求右子树距根结点最大距离,记为rightMaxDistance;则二叉树中结点最大距离满足,这两个距离的最大者+1。
如何消除嵌套括号
给定字符串“(1,(2,3),(4,(5,6),7))”,括号内元素可以是数字也可以是另一个数字。将之消除嵌套括号,变成"(1,2,3,4,5,6,7)",若是有误则报错。
要实现:判断表达式正确;消除嵌套括号。
判断正确:
表达式只有数字,逗号和括号,其他字符是非法
判断括号是否匹配。若是"(",括号计数器值加1;碰到")",再判断计数器值是否大于1,若是则计算器减1,否则非法。
当遍历表达式后,若括号计数器值为0,则说明括号匹配出现,否则是非法表达式
如何不用比较运算就可以求出两数最大与最小
Max(a,b)=(a+b+|a-b|)/2,Min(a,b)=(a+b-|a-b|)/2。
问题:a,b太大会溢出,所以a,b转换成长整型。
结语
本次博文对何昊出版的《java程序员面试宝典》的第8章数据结构部分的概括笔记,删除其中部分知识点,保留一部分代码(而且是核心代码),大部分代码不留存,只描述思想,希望对诸位有用。