位运算
原码、补码与反码
计算机中为了方便正数与负数运算,引用了补码
将数的二进制编码称为原码
1、正数的原码 = 反码 = 补码
2、将负数的二进制编码的数字位按位取反得到负数的反码
3、负数的补码 = 负数的反码(除符号位) + 1 = 它对应的正数(包括符号位取反) + 1
计算机存储负数时,存的是它的补码,正数和负数相加的结果 = 正数 + 负数的补码
eg:
这里假定是4位表示,第一位为符号位
+3 :
原码 :0011
-2:
原码:1010
反码:1101
补码:1110
-2 + 3 = 1 = 0001 = 1110 + 0011 = 0001
计算机存储负数时,存的是它的补码
由此带来两个性质:
A、在计算机中一个正数的相反数 = 正数按位取反 + 1,而一个负数的相反数 = (负数 - 1)再按位取反
B、-1的所有位都是1,按照性质A,-1 = 1按位取反 + 1 = 全1
int getPositive(int negative) {
// 输入一个负数得到它对应的正数
return ~(negative - 1);
}
int getNegative(int positive) {
// 输入一个正数得到它对应的负数
return ~positive + 1;
}
在二进制中还有一个性质,那就是一个数取反+1和-1后再取反是一样的:
~x + 1 = ~(x-1)
证明过程:
设原二进制数全0,取反加一和减一取反都等于1;假设原二进制数至少存在一个1,不妨按从右数第一个1为界将原二进制数分成左右两半,即X10…0形式。取反加一:(~ X01…1) + 1 = ~ X10…0,减一取反:~(X01…1) = ~X10…0,所以等价。
因此上边写的求正数的相反数和求负数的相反数本质是一样的!
故求相反数:
// 取相反数
int getOpposite(int num) {
return ~num + 1;
}
左移右移<<
& >>
计算机以2进制存储数据,因此对于int类型的数据,左移一位相当于乘2,右移一位相当于地板除2.
void testLeftRightMove()
{
int a = 4, b = 5;
int c = a >> 1, d = b >> 1;
int e = a << 1, f = b << 1;
cout << " a:" << a << " 右移一位:" << c << endl;
cout << " b:" << b << " 右移一位:" << d << endl;
cout << " a:" << a << " 左移一位:" << e << endl;
cout << " b:" << b << " 左移一位:" << f << endl;
}
因此求中间位置索引可以由下式:
int mid = L + ((R - L) >> 1);
无符号右移
关于一个数左右移位,左移必定是底位补0。
而右移如果是正数,高位补0,如果是负数则高位会补1,以保证移位不改变正负性。
在java中有一种无符号右移,>>>
,即去除符号位不管正负都是高位补0,C++中没有这种无符号右移,需要将一个数转为无符号数 unsigned_int,再右移。
异或运算^
- 位运算
相异为1,相同为0
。
也可理解为:
无进制相加
- 性质
-
任何数与0异或是本身
0 ^ N = N
与自身异或是0N ^ N = 0
-
满足交换律和结合律
a ^ b = b ^ a
a ^ b ^ c = a ^ (b ^ c)
-
由2可以一组数异或与它们的异或顺序无关
-
根据上述性质可以有一种不需要额外空间的数据交换的方式
void swap1(vector<int>& arr, int i, int j)
{
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
背后的原理:
这里做的前提是a和b分别属于自己的内存空间,a和b可以是相同的值,但必须分属自己的内存空间
。
因此运用在数据中, i和j不可相同
,否则会计算为0
;
由异或的性质还可应用于如下场景
1、已知在int数组中,只有一种数出现了奇数次,其他数都出现偶数次,采用时间复杂度为O(N)空间复杂度为O(1)的方法找到出现次数为奇数次的这个数
2、同样int数组,有两种数出现奇数次,其他数都出现偶数次,求这两种数
- 第一问由异或的性质知道出现偶数次的数字相当于与自身异或 得到0,出现奇数次数字异或得到自身,因此整个数组异或的结果就是该数本身
- 第二问根据第一问的思路可知整个数组异或的结果就是这两个奇数次的数a和b的异或eor,那么因为是两种数即a与b不相等,那么eor的结果≠0,那么eor至少有一位数是1,即非0,任意指定eor为1的那一位,根据数组中元素在这一位上的结果即可将数组中所有的数分为两类c1(该位为0),c2(该位为1),a和b必定分属c1和c2的一个,将c1和c2两组数整个异或,分别得到a和b。
第二问中比较重要的点:
- 取出eor为1的那一位
这里采用了int rightOne = eor & (~eor + 1);
来取出最右边的1 - 根据这一位分组,组内进行累计异或
if (((*it) & rightOne) == 1) // 根据这一位分组 { onlyOne ^= (*it); // 同组元素异或得到a或者b }
- 得到a ^ b和a或者b中的一个后,另一个即为两者再异或
cout << "第一种数:" << onlyOne << " 第二种数:" << (onlyOne ^ eor) << endl;
void printOddTimesNum1(vector<int> v)
{
int eor = 0;
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
eor = eor ^ (*it);
}
cout << "奇数次出现的数是:" << eor << endl;
}
void printOddTimesNum2(vector<int> v)
{
int eor = 0;
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
eor = eor ^ (*it);
}
// eor = a ^ b
// eor != 0
// eor 至少有一个位置上是1
int rightOne = eor & (~eor + 1); // 取出最右边的1
int onlyOne = 0;
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
if (((*it) & rightOne) == 1) // 根据这一位分组
{
onlyOne ^= (*it); // 同组元素异或得到a或者b
}
}
cout << "第一种数:" << onlyOne << " 第二种数:" << (onlyOne ^ eor) << endl;
}
位运算常用技巧
取相反数
int getOpposite(int num){
return ~num + 1;
}
反转0-1
利用任何数与0异或得本身,与本身异或得0的性质,将结果与1求异或,则将0 转为1,1转为0;
int flip(int n){
return n ^ 1;
}
判断负数与非负数
将符号位移到最右边,与1求与,如果是负数得到1,如果是正数得到0,再利用上边反转0-1的技巧,与1异或,将正数结果转为1,负数转为0,得到sign函数();
int sign(int n){
return ((n >> 31) & 1) ^ 1;
}
数组交换两元素位置
利用异或交换
void swap1(vector<int>& arr, int i, int j)
{
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
取一个数最右边的一个1
一个数取反+1与自身求与
int rightOne = num & (~num + 1);
去除最右边的1
假设一个n = 01101010,我们把它减一即n-1 = 01101001,然后再将它们相与result = n&(n-1) = 01101000,就可以把最后一个1消除了,原理也很简单,n-1的话,会一直向前寻找可借的位,从而跳过低位连续的0,而向最低位的1借位,借位后最低位的1变为0,原先最低位1的下一位从0变为1,其余位都不变,相与之后其它位不变,1(最低位1)0 &01(n-1对应的位)= 00,从而消除最低位的1。
利用这个原理计算二进制中1的个数
int countOne(int n){
int res = 0;
while(n!=0){
res++;
n = n&(n-1);
}
return res;
}
将一个数后n位置1,其他位置零
利用 100 - 1 = 11 ,1000 - 1 = 111
故将1左移n位,再减一即可。
一般int为8字节,32位,没法将1左移32位。
而所有位都是1的数,实际上就是-1,因为计算机存储负数是它的补码, - 1 的原码是100……1
反码是111……0,补码是反码 + 1 故是全1.
int limit = n== 32 ? -1 : (1 << n) - 1;
位运算题目案例
返回较大值
给定两个有符号32位整数a和b,返回a和b中较大的。
【要求】
不用做任何比较判断。
查看 a - b的正负即可,如果a-b >= 0,返回a,否则返回b。
bitMax1()直接相减,有可能会导致溢出问题。
bitMax2()考虑到溢出,正数 - 负数以及 负数 - 正数时,可能出现溢出。但这两种情况实际上可以直接通过符号为判断出较大者。
// 输入0输出1,输入1输出0
int flip(int n){
return n ^ 1;
}
// 取出符号位,非负数返回1,负数返回0
int sign(int n){
return filp( (n >> 31) & 1);
}
int bitMax1(int a, int b){
int c = a - b;
int sa = sign(c); // 判断sa的符号
int sb = flip(sa); // 赋值一个数与sa互斥
return a*sa + b*sb; // 互斥结果相加,必定只返回其中一个
}
int bitMax2(int a, int b){
int c = a -b;
// 不管溢出,先求a,b,a-b的符号
int sa = sign(a);
int sb = sign(b);
int sc = sign(c);
//设定互斥系数代表a和b符号是否一致
int difSab = sa ^ sb; // a和b符号不一样
int sameSab = filp(difSab); // a和b符号一样
int returnA = difSab*sa + sameSab*sc; // a为非负数且a,b符号不相同,返回a,或者a,b符号相同,c符号为非负数也返回a
int returnB = filp(returnA); // B的返回与A返回互斥
return a*returnA + b*returnB;
}
判断一个数是否为2的幂、4的幂
【题目】判断一个数是否为2的幂,4的幂
首先
幂结果都是非负数。
2的幂只有一位是1,其余位是0,因此,使用取它右边的1的方法,得到rightOne,如果这个数等于rightOne,则返回true,否则false
return n == (n & (~n + 1));
或者可以发现,2的幂-1与2的幂刚好是1错开的情况,故假如一个数x与x-1求与结果为0则是2的幂,否则不是。
return x&(x-1) == 0;
至于判断是否为4的幂,先判断根据上边思路rightOne是否等于本身,判断是否为2的幂,随后
将这个数与 0101……0101(32位,0-1交替出现) 与运算,如果结果不为0,则是4的幂,否则不是。
// 判断一个数n是否为2的幂
bool is2Power(int n){
if (n <= 0){
return false;
}
// return n == (n & (~n + 1));
return (n & (n - 1)) == 0;
}
//
bool isPowerOfFour(int n) {
if (n <= 0){
return false;
}
// 是2的幂,且与0101……0101(这个数等于十六进制8个5)与运算不为0
//
return ((n & n-1) == 0) && ((n & 0x55555555) != 0);
}
不用算术运算符实现四则运算
【题目】给定两个有符号32位整数a和b,不能使用算术运算符,分别实现a和b的加、减、乘、除运算
【要求】
如果给定a、b执行加减乘除的运算结果就会导致数据的溢出,那么你实现的函数不必对此负责,除此之外请保证计算过程不发生溢出
首先位运算基本的运算是,与或非,异或,同或
其中两数异或 = 两数无进位相加
两数求与得1的数位,表明该位满2需要向前一位进位。
故进位信息就是两数求与在左移一位。
故:
【加法】两数相加 = 两数 ^ + (两数&) << 1
这样一来,两数相加转为另外两数相加,继续进行这样的转化,直至某次发现进位信息为0,则两数相加直接等于两数无进位相加。
// 加法:两数加 = 异或(无进位加)+ 与 左移 1(进位信息),当进位信息为0时,停止转换
static int add(int a, int b) {
int sum = a;
while (b != 0) {
sum = a ^ b;
b = (a & b) << 1;
a = sum;
}
return sum;
}
【减法】a - b = a + (-b)
故可以由加法位运算和取相反数的位运算来实现减法位运算。
// 获取一个数的相反数
static int opposite(int num) {
return ~num + 1;
}
// 减法:a-b 等于 a + b的相反数
static int minus(int a, int b) {
return add(a, opposite(b));
}
【乘法】
二进制乘法和十进制乘法运算规则是一致,将乘数(下边的)上的每一位与被乘数(上边的)相乘,所得结果乘于进制(十进制×10,二进制×2)的N次幂(N为位数,右起第一位为0,第二为1)。所得结果累计求和就是所求。
二进制中数位要么是0,要么1,乘0就是+0,乘1就是加这个数本身。
因此二进制乘法可以由加法 + 移位操作完成
// 乘法:
static int multiple(int a, int b) {
// 取b的每一位与a乘,这个结果,如果改位为0就是0,如果是1就是a本身,然后左移N位
int ans;
while (b != 0) {
if ((b & 1) != 0) {
ans = add(ans, a);
}
// 每次a左移一位,b右移一位
a <<= 1;
b >>= 1; // 这里有点问题,实际上需要无符号右移
}
}
【除法】
除法的过程可以由乘法的过程求逆运算来解。
乘法的结果是一个一个 被乘数左移位次相加得来。
那么在除法中,我们将除数尽可能左移的同时保证它依旧小于被除数,此时移动到的数位在最后的结果上是1.随后将被除数减去这个移位后的除数,继续执行上边的操作。
/*
使用位运算实现四则运算
*/
class addMinusMultiDivideByBit {
public:
// 加法:两数加 = 异或(无进位加)+ 与 左移 1(进位信息),当进位信息为0时,停止转换
static int add(int a, int b) {
int sum = a;
while (b != 0) {
sum = a ^ b;
b = (a & b) << 1;
a = sum;
}
return sum;
}
// 获取一个数的相反数
static int opposite(int num) {
return ~num + 1;
}
// 减法:a-b 等于 a + b的相反数
static int minus(int a, int b) {
return add(a, opposite(b));
}
// 乘法:
static int multiple(int a, int b) {
// 取b的每一位与a乘,这个结果,如果改位为0就是0,如果是1就是a本身,然后左移N位
int ans;
while (b != 0) {
if ((b & 1) != 0) {
ans = add(ans, a);
}
// 每次a左移一位,b右移一位
a <<= 1;
b >>= 1; // 这里有点问题,实际上需要无符号右移
}
}
static bool isNeg(int n) {
return n < 0;
}
// 除法
static int divide(int a, int b) {
int x = isNeg(a) ? opposite(a) : a;
int y = isNeg(b) ? opposite(b) : b;
int res = 0;
for (int i = 31; i > -1; i = minus(i, 1)) {
if ((x >> i) >= y) {
res |= (1 << i);
x = minus(x, y << i);
}
}
return isNeg(a) ^ isNeg(b) ? opposite(a) : a;
}
};
二分查找
二分的精髓在于每次将搜寻范围缩减一半,因此时间复杂度为O(log2(N)),对于二分问题重点在于如何缩减范围,缩减的边界。
对数器
测试方法与标准方法(比较可靠的方法)应用于同样的测试案例,重复随机生成大量的测试案例,来看两个方法是否保持一致。
对于排序先生成随机长度,随机的数组
vector<int> generateRandomArr(int maxSize, int maxVaule)
{
vector<int> v1;
int n = rand() % maxSize + 1; //长度随机 1 - maxSize
v1.resize(n);
for (int i = 0; i < n; i++)
{
v1[i] = (rand() % maxVaule) - (rand() % maxVaule); // 生成随机数
}
return v1;
}
利用自带库函数作为对比方法:
vector<int> compareSorter(vector<int> arr)
{
sort(arr.begin(), arr.end());
return arr;
}
测试:
srand((unsigned int)time(NULL)); // 设置随机数种子为当前系统时间(unsigned int)强制转换
int testTime = 500000;
int maxSize = 10, maxValue = 200;
bool selectFlag = true;
bool bubbleFlag = true;
bool insertFlag = true;
for (int i = 0; i < testTime; i++)
{
vector<int> arr = generateRandomArr(maxSize, maxValue);
vector<int> copyArr;
copyArr.resize(arr.size());
copy(arr.begin(), arr.end(), copyArr.begin());
vector<int> compareArr = compareSorter(copyArr);
vector<int> selectSortArr = selectSort(arr);
vector<int> bubbleSortArr = bubbleSort(arr);
vector<int> insertSortArr = insertSort(arr);
//cout << "原数组:" << endl;
//printArr(arr);
//cout << "拷贝数组:" << endl;
//printArr(copyArr);
//cout << "标准排序数组:" << endl;
//printArr(compareArr);
//cout << "选择排序组:" << endl;
//printArr(selectSortArr);
//cout << "冒泡排序组:" << endl;
//printArr(bubbleSortArr);
//cout << "插入排序组:" << endl;
//printArr(insertSortArr);
if (selectSortArr != compareArr)
{
selectFlag = false;
break;
}
if (bubbleSortArr != compareArr)
{
bubbleFlag = false;
break;
}
if (insertSortArr != compareArr)
{
insertFlag = false;
break;
}
}
cout << selectFlag << endl;
cout << (selectFlag && bubbleFlag) << endl;
cout << (selectFlag && bubbleFlag && insertFlag) << endl;
O(N^2)的排序
选择排序
-
思路:
每次假定首元为min,遍历比较找到真实的min,交换min所在到数组头,缩减区间,找次小值
-
循环起止:
外层循环以假定的首元min开始,只剩下一个元素时不需要再排序,i < arr.size() - 1
不必取到最后一个元素索引( arr.size() - 1,故是<而非 <= )
内层循环将假定首元最小值与剩余值一一对比,故需要对比的值从 i + 1 开始 ,遍历数组结束 故 j = i + 1; j < arr.size()
void swap(vector<int>& arr, int i, int j)
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 选择排序
// 每次假定首元为min,遍历比较找到真实的min,缩减区间,找次min
void selectSort(vector<int>& arr)
{
if (arr.size() < 2)
{
return;
}
for (int i = 0; i < arr.size() - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < arr.size(); j++)
{
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
冒泡排序
-
思路
遍历数组,比较相邻元素,交互不符合排序规则(升序)元素,每轮相当于将最大值移动到数组末尾,缩减区间,再次遍历
-
循环起止
外层循环实质上每次将数组的最大值移动到数组末尾,仅剩一个元素的时候没必要移动,故外层循环从i = 0; i < arr.size() - 1
,不包含arr.size() - 1
内层循环比较相邻两元素大小 ,j 与 j+1
,
外层1轮,j从0-arr.size() - 1
不包含arr.size() - 1
外层2轮j从0-arr.size() - 2
不包含arr.size() - 2
……
故j = 0; j < arr.size() - 1 - i
// 冒泡排序
// `遍历数组,比较相邻元素,交互不符合排序规则(升序)元素,每轮相当于将最大值移动到数组末尾,缩减区间,再次遍历`
vector<int> bubbleSort(vector<int> arr)
{
if (arr.size() < 2)
{
return arr;
}
for (int i = 0; i < arr.size() - 1; i++)
{
for (int j = 0; j < arr.size() - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
swap(arr, j, j + 1);
}
}
}
return arr;
}
插入排序
- 思路:
遍历数组,指定第一个元素为有序数组,将剩余元素视为待插入数据,遍历有序数组中元素对比插入到有序数组中,扩充有序,减少无序
- 循环起止
外层循环遍历无序数组,起始假定0位置为有序数组故无序数组索引从1开始到末尾结束int i = 1; i < arr.size()
,内层循环将当前待插入数据与有序数组中每个元素对比,故遍历有序数组起止即为有序数组的长度int j = 0; j < i;
// 插入排序
// `遍历数组,指定第一个元素为有序数组,将剩余元素视为待插入数据,遍历有序数组中元素对比插入到有序数组中,扩充有序,减少无序`
vector<int> insertSort(vector<int> arr)
{
if (arr.size() < 2)
{
return arr;
}
for (int i = 1; i < arr.size(); i++)
{
for (int j = 0; j < i; j++)
{
if (arr[j] > arr[i])
{
swap(arr, i, j);
}
}
}
return arr;
}
上边写的内存循环没有利用到有序数组的有序性,实际上当前待插入元素a[i]只需要从有序数组的末尾(最大值)遍历到第一个小于a[i]的元素arr[j] > arr[j + 1];
即可,将a[i]插入该元素后边
因此上述插入排序,内循环
for (int j = 0; j < i; j++)
{
if (arr[j] > arr[i])
{
swap(arr, i, j);
}
}
可以简写为:
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--)
{
swap(arr, j, j + 1);
}
这样实际上可以提前终止循环,因此算法效率更好,在最好的情况下复杂度是O(n)
递归
采用递归的方式来求数组的最大值:
每次取区间中点进行拆分成左右两部分,求左右两部分的最大值,拆分到最后必定出现左右两部分都只剩下一个元素,返回左右两部分最大值的较大者。
栈树:
递归行为时间复杂度估算
master 公式
T(N) = a * T(N / b) + O(N^d)
子问题等规模的时候可以应用master公式求解时间复杂度
对于上述问题:
子问题是分别求左右两边的最大值,问题规模缩减为母问题的一半,调用两次,求左右最大值和重点操作是O(1),带入master公式:
堆(heap)
树
完全二叉树与满二叉树
- 堆结构是用数组实现的完全二叉树
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为
完全二叉树
。
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。称为
满二叉树
- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
可以将一段连续空间的数组想象为一颗完全二叉树,并且第i个位置的左节点、右节点和父节点有如下对应关系:
根据节点在完全二叉树中的位置(深度d和所在深度的序号s,0开始)可以确定节点对应的连续数组的索引为 2^(d) + s,而由节点位置可以确定父子节点的位置,因此可以推导出上图中的结论公式:
- i位置的左节点为
2*i+1
- i位置的右节点为
2*i+2
- i位置的父节点为
(i - 1)/ 2
- 完全
##大根堆和小根堆
- 大根堆
首先堆是完全二叉树,其次大根表示每棵树的最大值就是根节点
例如:
- 小根堆
同理每棵子树的最小值都是其根节点。
heapInsert与heapify
heapInsert:向大根堆中插入一个数字,使得堆依旧是大根堆
//heapInsert:某个数在index上,往上移动,使得整个堆依旧是大(小)根堆
void heapInsert(vector<int>& arr, int index)
{
while (arr[index] > arr[(index - 1) / 2]) // 若当前值 > 父节点(此条件隐含了上移到根节点时终止)
{
swap(arr, index, (index - 1) / 2); // 将当前值交换
index = (index - 1) / 2; // 上移
}
}
heapify:将index上的数下移,使得堆依旧是大根堆
// heapify: 某个数在index位置,往下移动,使得整个堆依旧是大(小)根堆
void heapify(vector<int>& arr, int heapSize, int index)
{
int left = 2 * index + 1; // 左孩子索引
while (left < heapSize) // 判断是否越界,若已经没有孩子节点,结束
{
// 如果有右孩子且右孩子大于左孩子,largestIndex是右孩子索引
int largestIndex = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父节点和孩子节点中的较大值比较,若父节点小,将父节点下移,否则
largestIndex = arr[largestIndex] > arr[index] ? largestIndex : index;
if (largestIndex == index)
{
break; // 若父节点已经是大于左右较大值,则结束移动
}
swap(arr, largestIndex, index); // 否则,下移index指向的元素
index = largestIndex;
left = index * 2 + 1;
}
}
O(NlogN)的排序
归并排序
- 思路
二分数组,将排序转为,分别使二分后的左右两数组有序,然后将两有序数组归并成一个有序。
- 递归体
确定区间中点m,不断将数组二分为L—— m,m+1 —— R,直至数组中仅包含一个元素 - 递归出口
执行两有序数组的归并操作,每次从左右数组头开始遍历,比较左右大小,按序放入辅助空间中。
带入master公式,复杂度为O(Nlog(N))
// 归并排序
//
// arr的L-M 和M-R分别有序了,现要求整体有序
void myMerge(vector<int>& arr, int L, int M, int R)
{
vector<int> help;
help.resize(R - L + 1); // 开辟辅助空间help存储整体有序的数组
int i = 0; // 辅助数组辅助计数器
int p1 = L, p2 = M + 1;
while (p1 <= M && p2 <= R) // 构建两指针遍历两的子数组
{
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
} // 该循环在遍历完成其中一个有序数组后结束
// 将未遍历完的数组依次赋值给help
while (p1 <= M)
{
help[i++] = arr[p1++];
}
while (p2 <= R)
{
help[i++] = arr[p2++];
}
// 将排好序的help赋值给arr
for (i = 0; i < help.size(); i++)
{
arr[L + i] = help[i];
}
}
// `二分数组,将排序转为,分别使二分后的左右两数组有序,然后将两数组归并成一个。`
void mergeSortProcess(vector<int>& arr, int L, int R)
{
if (L == R)
{
return;
}
int mid = L + ((R - L) >> 1);
mergeSortProcess(arr, L, mid);
mergeSortProcess(arr, mid + 1, R);
myMerge(arr, L, mid, R);
}
vector<int> mergeSort(vector<int> arr)
{
if (arr.size() < 2)
{
return arr;
}
mergeSortProcess(arr, 0, arr.size() - 1);
return arr;
}
拓展一——小和问题
-
思路
从小和求解的过程可以看出,1出现4次,是因为1右边比它大的数有4个,3出现2次,因为3右边比它大的有2个,由此可知小和问题的等价解是求一个数右边 有多少个数比它大。
在归并排序中,我们每次merge的都是两个有序数组,因此统计一个数右边 有多少个数比它大可直接利用下标得到因此,采用归并问题求解小和可以得到O(NlogN)的解法。 -
递归体
二分数组,求各自的小和,直至数组中只包含一个元素返回0 -
递归出口
归并左右的有序数组,统计右边大于左边当前数的个数,乘当前数,求小和
smallSum += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
merge的时候需要赋值排序,这样才能保证可以直接根据索引计算右边大于左边当前数的个数。
// 归并排序拓展一——小和问题
int smallSumMerge(vector<int> &arr, int L, int m, int r)
{
// 在归并排序的同时求小和
vector<int> help;
help.resize(arr.size());
int i = 0; // 归并赋值计数器
int p1 = L, p2 = m + 1; // 左右两有序数组指针,起始位置在数组头部
int smallSum = 0; // 统计右边有多少个数比当前数大,则有多少个小和
while (p1 <= m && p2 <= r)
{
// 与经典merge不同,这里必须保证右组的数严格大于左组的数才可以知道右边大于当前数的个数
// 即当左右两数相等时,先将右组数拷贝到help数组中
smallSum += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m)
{
help[i++] = arr[p1];
}
while (p2 <= r)
{
help[i++] = arr[p2];
}
return smallSum;
}
// 对arr的L-r范围内既要排序也要求小和
int smallSumProcess(vector<int> &arr, int L, int r)
{
if (L == r)
{
return 0;
}
int m = L + ((r - L) >> 1);
// 小和是左组小和加右组小和加merge后的小和
return smallSumProcess(arr, L, m) + smallSumProcess(arr, m + 1, r) + smallSumMerge(arr, L, m, r);
}
int smallSum(vector<int>arr)
{
if (arr.size() < 2)
{
return 0;
}
return smallSumProcess(arr, 0, arr.size() - 1);
}
拓展二——逆序对
快速排序
引子——putLeftRight
双指针,一个指向划分≤区的右边界j
,一个指向已经遍历的数组i
其实i,j都指向第一个元素的左边界(0索引元素左边),遍历数组,碰到≤target的数,将该数与j
指向的下一个元素交换(一开始,即为0号元素),扩充≤区(j++
).
vector<int> putLeftRight(vector<int> arr, int target)
{
for (int i = 0, j = 0; i < arr.size(); i++)
{
if (arr[i] <= target)
{
//int tmp = arr[i];
//arr[i] = arr[j];
//arr[j] = tmp;
//j++;
swap(arr, i, j++);
}
}
return arr;
}
引子——putLeftMidRight
// 三指针,i用于遍历数组,s指向<区右边界(起始为0的左边),g指向>区左边界(起始为arr.size的右边),当前元素<target,将该元素与<区下一个元素交换,扩充<区(s++),== target,跳过,>target,将该元素与>区的前一个元素交换,扩充>区,g--,i此时不变,直至i与k相等
// 将上述问题的≤区再拆分为<区和=区,即划分为<区和=区和>区域
vector<int> putLeftMidRight(vector<int>& arr, int target)
{
for (int i = 0, s = 0, g = arr.size(); i < g;)
{
if (arr[i] < target)
{
swap(arr, i++, s++);
}
else if (arr[i] == target)
{
i++;
}
else
{
swap(arr, i, --g); //>区交换过来的元素还没有进行检查,因此大于的时候i不变
}
}
return arr;
}
快速排序v1
应用引例一得快排v1版本:
每次假定最后一个数为target,应用引例一划分≤区,将≤区与target交换,则搞定target的最终位置,分别在≤区和>区再次应用引例一划分,直至所有数字最终位置都确定
快速排序v2
应用引例二得快排v2版本:
与v1相比v2一次搞定一批数字的位置
快速排序v3
快速排序v1最差情况下时间复杂度为O(N)因为每轮只完成了一个(一种)数字的排序,假定目标选择使得>区与<区划分的情况较差,就容易造成较差的情况。当目标选择是随机的情况下,可以实现时间复杂度O(Nlog(N))
pair<int, int> fastPut(vector<int>& arr, int L, int r)
{
int s = L - 1; // <区右边界
int g = r; // >区左边界
while (L < g)
{
if (arr[L] < arr[r])
{
swap(arr, ++s, L++);
}
else if (arr[L] == arr[r])
{
L++;
}
else
{
swap(arr, L, --g);
}
}
swap(arr, g, r);
return make_pair(s + 1, g);
}
void fastProcess(vector<int> &arr, int L, int r)
{
if (L < r)
{
// 随机生成target的索引 L~r
int index = (rand() % (r - L + 1)) + L;
swap(arr, index, r);
pair<int,int> index_pair = fastPut(arr, L, r); // 获取放置后的 = 区的首尾索引
fastProcess(arr, L, index_pair.first - 1); // 递归<区
fastProcess(arr, index_pair.second + 1, r); // 递归 > 区
}
}
// 快速排序v3
vector<int> fastSort(vector<int> arr)
{
if (arr.size() < 2)
{
return arr;
}
fastProcess(arr, 0, arr.size() - 1);
return arr;
}
堆排序
大根堆的heapInsert和heapify操作复杂度都是logN级别,先一个一个的heapInsert将数组变成一个大根堆,将根节点与最后一个位置交换,确定出一个最大值,heapSize--
,执行heapify,再次让数组变为大根堆,确定最大值,直至heapSize减值1.
// 堆(heap)
//heapInsert:某个数在index上,往上移动,使得整个堆依旧是大(小)根堆
void heapInsert(vector<int>& arr, int index)
{
while (arr[index] > arr[(index - 1) / 2]) // 若当前值 > 父节点(此条件隐含了上移到根节点时终止)
{
swap(arr, index, (index - 1) / 2); // 将当前值交换
index = (index - 1) / 2; // 上移
}
}
// heapify: 某个数在index位置,往下移动,使得整个堆依旧是大(小)根堆
void heapify(vector<int>& arr, int heapSize, int index)
{
int left = 2 * index + 1; // 左孩子索引
while (left < heapSize) // 判断是否越界,若已经没有孩子节点,结束
{
// 如果有右孩子且右孩子大于左孩子,largestIndex是右孩子索引
int largestIndex = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父节点和孩子节点中的较大值比较,若父节点小,将父节点下移,否则
largestIndex = arr[largestIndex] > arr[index] ? largestIndex : index;
if (largestIndex == index)
{
break; // 若父节点已经是大于左右较大值,则结束移动
}
swap(arr, largestIndex, index); // 否则,下移index指向的元素
index = largestIndex;
left = index * 2 + 1;
}
}
// 堆排序
vector<int> heapSort(vector<int> &arr)
{
if (arr.size() < 2)
{
return arr;
}
// 将数组一个一个插入转为大根堆
for (int i = 0; i < arr.size(); i++)
{
heapInsert(arr, i);
}
int heapSize = arr.size();
while (heapSize)
{
swap(arr, 0, --heapSize); // 将最大值交换到最后,排好一个数,heapSize - 1
heapify(arr, heapSize, 0); // 执行heapify下移头结点,重新构大根堆
}
return arr;
}
堆排序扩展
桶排序
桶”是一个区间范围,里面可以承载一个或多个元素。桶排序的第一步就是确定桶的个数和区间。具体的建立多少个桶、每个桶的区间范围是多少,有不同的方式
计数排序
数据类似年龄等数字分布在一个固定的区间中,那么即可通过统计数字出现的次数来排序。
基数排序
依次按照个位、十位、……最高位数字进桶、出桶排序数字,相当于一位一位的比较,桶其实就是存放数字的容器,这里用的容器应该先进先出。因为桶本身实际上是有序的,入桶的过程实质上对某个数位的数字进行了排序归并,从低位向高位,是优先级的过程。
// 基数排序
// 求数组中最大数字的数位个数
int maxBits(vector<int> arr)
{
int max = 0;
for (int i = 0; i < arr.size(); i++)
{
max = arr[i] > max ? arr[i] : max;
}
int res = 0;
while (max)
{
res++;
max /= 10;
}
return res;
}
// 取出数字的第d位数字
int getDigit(int num, int d)
{
// 进行 d - 1 次除10后,取余数
for (int i = 1; i <= d - 1; i++)
{
num /= 10;
}
return num % 10;
}
// 基数排序处理
void radixSortProcess(vector<int>& arr, int L, int r, int maxBits)
{
const int radix = 10;
int i = 0, j = 0; // 临时变量,i用于指针数组索引,j用于存储每位数字
// 有多少个数就准备多少个辅助空间,用于存放进桶出桶后的数组
vector<int> help;
help.resize(r - L + 1);
for (int d = 1; d <= maxBits; d++)
{
vector<int> count;
count.resize(12);
// 左到右遍历数组统计数字,相当于进桶
for (i = L; i <= r; i++)
{
j = getDigit(arr[i], d);
count[j]++;
}
// 累加词频统计数组count
for (i = 1; i < radix; i++)
{
count[i] = count[i] + count[i - 1];
}
// 右到左遍历数组,相当于出桶
for (i = r; i >= L; i--)
{
j = getDigit(arr[i], d);
// 累计数组告诉count[j]表示 ≤j 的数字个数,插入索引为count[j] - 1
help[count[j] - 1] = arr[i];
count[j]--;
}
// 将help数组赋值给arr
for (i = L, j = 0; i <= r; i++, j++)
{
arr[i] = help[j];
}
}
}
// 基数排序主函数
vector<int> radixSort(vector<int> arr)
{
if (arr.size() < 2)
{
return arr;
}
radixSortProcess(arr, 0, arr.size() - 1, maxBits(arr));
return arr;
}
- 基数排序本身只适用于正数的排序,为了排序含有负数的数组,可以先求出最小值,整个数组加上最小值的绝对值将数组转为非负数数组,进行基数排序后,整个数组再减去最小值的绝对值
- 上边的函数是通过词频数组来模拟入桶出桶的过程,将词频数组累计求和就可以判断某个数应该归入的范围,从右向左,后序遍历,然后词频递减,保证后边的数(后入桶)排序在后边。
排序算法的稳定性
所谓稳定性是指排序前后数组中数字的相对顺序不变,比如数组中出现了两个3,两个3排序前后的相对顺序不变。
** 各种基于比较的排序算法,如果交换方式是相邻交换一步一步将数字移动到正确顺序的位置,那么只要在比较时碰到两者相等的情况不交换,就可以保证稳定性**,如插入排序,冒泡排序,归并排序。
如果是跳跃式交换,则会破话稳定性,如选择排序假定最值与当前值的交换,快排的≤区边界与待排数据的交换,堆排序父子节点的交换。
- 选择排序无法做到稳定,有跳跃的交换
- 冒泡排序可以稳定,冒泡排序相邻元素比较后交换,如果相等不必交换就可以保持相对顺序不变。
- 插入排序可以稳定,新的未排元素向有序部分插入的时候是相邻交互插入的,当相等的时候不交换就可以保证稳定性
- 归并排序可以稳定,在merge的时候,当相等的时候先放左边数组的数再放右边即可保证稳定性。
- 快速排序做不到稳定性
- 堆排序无法做到稳定性。
- 桶排序可以做到稳定性,先进先出即可。
排序算法总结
排序算法比较
一般来讲选择快速排序算法排序,有空间要求则选择堆排序,有稳定性要求选择归并排序。
5其实与快速排序将数组划分为≤某个数放左边,大于某个数放右边这种方式类似,快速无法做到稳定性,那么5也是无法做到稳定性的。
排序算法结合
快速排序在数据状况比较有序的情况下操作次数会少,因此可以在小样本情况下使用插入排序来使其跑得更快。
//if (r - L < 60)
//{
// // 在 r - L区间插入排序
// return;
//}
void fastProcess(vector<int> &arr, int L, int r)
{
//if (r - L < 60)
//{
// // 在 r - L区间插入排序
// return;
//}
if (L < r)
{
// 随机生成target的索引 L~r
int index = (rand() % (r - L + 1)) + L;
swap(arr, index, r);
pair<int,int> index_pair = fastPut(arr, L, r); // 获取放置后的 = 区的首尾索引
fastProcess(arr, L, index_pair.first - 1); // 递归<区
fastProcess(arr, index_pair.second + 1, r); // 递归 > 区
}
}
对有稳定性要求的自定义数据进行归并排序,没有要求的基本数据类型用快速排序。
链表
删除节点
对于头结点head,删除它的方式是head = head->next
,对于中间节点D,删除它的方式找到它的前一个节点pre
,然后pre -> next = pre -> next -> next
,跳过节点D即可。
为了统一这两种情况:可以采用虚拟头节点dummyNode指向head
删除节点的另一种方式
若链表中的某个节点,既不是链表头节点,也不是链表尾节点,则称其为该链表的「中间节点」。
假定已知链表的某一个中间节点,请实现一种算法,将该节点从链表中删除。
例如,传入节点 c(位于单向链表 a->b->c->d->e->f 中),将其删除后,剩余链表为 a->b->d->e->f
void deleteNode(ListNode* node) {
}
删除节点的通用做法是将当前节点cur
的前一个节点pre
的next指针指向当前节点的next
,即:
pre->next = cur->next;
而这里只给定要删除的节点,并且只知道该节点为中间节点。一种比较巧妙的删除方式是将该节点cur
赋值为它的下一个节点next
,将当前节点作为pre
,删除当前节点的下一个节点即可。
这个过程相当于保留cur
为cur->next
,随后删除cur->next
即,cur->next = cur->next->next;
void deleteNode(ListNode* node) {
node->val = node->next->val;
node->next = node->next->next;
}
增加节点
在某个节点C后边增加节点F,操作是F->next = C-> next; C->next = F
,如果要在头位置增加节点G,则G->next = head; head = G;
同样可以采用虚拟节点dummyNode
来统一这两种情况
哈希表
unordered_map
哈希表的增删改查都是常数级别的时间复杂度。
有序表
双指针,因为有序,要求公共部分,指针指向元素比较,谁小则弹出谁的头部,相等则打印,都弹出。
// 打印有序链表的公共部分
void printCommonListnode(list<int> n1, list<int> n2)
{
while (n1.size() > 0 && n2.size() > 0)
{
if (n1.front() == n2.front())
{
cout << n1.front() << endl;
n1.pop_front();
n2.pop_front();
}
else if (n1.front() > n2.front())
{
n2.pop_front();
}
else
{
n1.pop_front();
}
}
}
判断链表是否回文
-
直接做法遍历链表,压入栈中,再次遍历,同时与出栈元素比较是否相等,所有都相等则是回文。空间复杂度O(N)
-
对称做法,只把后一半元素入栈,然后遍历前一半元素,出栈对比是否相等,所有都相等是回文。问题是链表无法获取数据长度,怎么知道链表的中心位置呢?
快慢指针
可以用来解决这个问题,快指针一次走两步,慢指针一次走一步,当快指针走到尽头,慢指针来到链表中点,将慢指针后边的元素压入栈中,完成对比操作即可,空间复杂度O(N/2) -
逆序遍历的做法,先快慢指针,让快指针走到链表尾,慢指针走到链表中间,再来个指针从开始往中间走,快指针往回走,一个一个比较是否相等。空间复杂度O(1)。
// 1、入栈出栈比较
bool isPalindrome1(ListNode head)
{
stack<int> stk; // 构建栈
ListNode cur = head;
while (cur.next != NULL)
{
stk.push(cur.val);
cur = *cur.next;
}
while (cur.next != NULL)
{
if (cur.val != stk.top())
{
return false;
}
stk.pop();
}
return true;
}
// 2、快慢指针入栈出栈比较
bool isPalindrome2(ListNode head)
{
stack<int> stk; // 构建栈
ListNode slow = head, fast = head;
while (slow.next != NULL && fast.next->next != NULL)
{
slow = *slow.next;
fast = *fast.next->next;
}
// 快指针此时在链表尾或者表尾前一个,慢指针此时在链表中央或者中间第一个。
ListNode cur = slow;
// 慢指针遍历到结束,右边部分入栈
while (cur.next != NULL)
{
stk.push(cur.val);
}
// 从头遍历到慢指针,与出栈元素对比
ListNode cur = head;
while (cur.next != slow.next)
{
if (cur.val != stk.top())
{
return false;
}
stk.pop();
}
return true;
}
// 3、快慢指针,快指针逆序遍历比较
bool isPalindrome3(ListNode head)
{
ListNode n1 = head, n2 = head;
while (n1.next != NULL && n2.next->next != NULL)
{
n1 = *n1.next;
n2 = *n2.next->next;
}
n2 = *n1.next; // fast -> right part first node
n1.next = NULL; // mid.next -> null
ListNode n3 = NULL;
// 右边部分指向逆序,先保存当前指向的下一个节点,将指向改为指向前一个,右移节点,重复操作
while (n2.next != NULL) // right part convert
{
n3 = *n2.next; // n3 -> save next node
n2.next = &n1; // next of right node convert
n1 = n2; // move slow
n2 = n3; // move fast
}
n3 = n1;
n2 = head;
bool res = true;
while (n1.next != NULL && n2.next != NULL)
{
if (n1.val != n2.val)
{
res = false;
break;
}
n1 = *n1.next; // right to mid
n2 = *n2.next; // left to mid
}
// 恢复链表右边部分的指向
n1 = *n3.next;
n3.next = NULL;
while (n1.next != NULL)
{
n2 = *n1.next;
n1.next = &n3;
n3 = n1;
n1 = n2;
}
return res;
}
单链表按照某值划分片区
- 赋值给数组,按照荷兰国旗的做法排好,再将数组串成链表
- 不赋值给数组,给定6个指针指向 每个片区的首尾,每个片区各自串联,最后将片区串联
复制含有随机指针的链表
- 使用额外空间则采用哈希表,哈希表的键值为老节点,value值是赋值新的节点,根据老节点的指向来连接新节点。
- 不采用额外节点的方法则可以将新节点复制到老节点后边,根据老节点构建新节点的随机指针的连接关系,然后在next指针上分离新老链表。
两单链表相交
先判断是否有环
- 使用额外空间复杂度的做法是将链表放入哈希表中,每次放之前判断表中是否已经有了该节点,如果链表有环,则第一次发现哈希表中已经有了该节点的就是环相交的地方。
- 不使用额外空间的做法是采用快慢指针的做法,快指针每次两步,慢指针每次一步,如果有环,则快指针和慢指针一定会相遇。相遇后将快指针置零(放到head),快慢指针每次都走一步,那么两指针会在入环节点处相遇。
判断链表是否有环后,情况分为3种:
- 两无环链表
两链表如果相交最后必定是公共部分。
那么即可先遍历各自的链表得到两链表的长度,获取两链表长度的差值,长链表先走差值的距离后,短链表开始与长链表一块走,最后两链表一定同时结束,那么一定也会在某个时刻在公共交叉点相遇。
下边代码中复用了n来统计链表长度,n先遍历链表1自增,然后遍历链表2自减,n的绝对值即为链表长度的差值,n的正负可以区分链表的长短。
- 两有环链表
有三种情况,a、不相交b、入环节点相同c、入环节点不在一块。
b的判断可由入环节点是否相同来判断,b这种情况,求相交的思路与无环类似,只需要将两链表入环节点作为无环时候的终点,按照类似的方式来求。
a、这种情况各自遍历一遍链表,如何都没有相同,则是不想交。
c、这种情况相交的节点就是两个入环节点。
- 一个有环一个无环
这种情况下两链表不可能相交!
二叉树
二叉树的遍历顺序
- 递归顺序
每个节点先遍历左,直至左为空(到最底层左边),然后遍历右。
遍历会三次进入递归的主函数。
在递归顺序的基础上根据头左右的先后顺序,又分为先序(递归序中第一次出现即打印,头,左,右),中序(递归序中第二次出现即打印,左,头,右),后序(递归序中最后一次出现即打印,左右头)。所谓前中后,也就是递归主函数中执行的顺序,或者说是头结点的顺序。
递归实现的遍历:
只需要调整arr.push_back(node->val);
操作的顺序即可。
class Solution {
public:
void preOrder(TreeNode* node, vector<int> &arr){
if (!node){
return;
}
arr.push_back(node->val); // 先序在递归前操作
preOrder(node->left, arr);
preOrder(node->right, arr);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (!root){
return res;
}
preOrder(root, res);
return res;
}
};
class Solution {
public:
void inorder(TreeNode* node, vector<int> &arr){
if(!node){
return;
}
inorder(node->left, arr);
arr.push_back(node->val); // 中序遍历在递归左后操作
inorder(node->right, arr);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
};
class Solution {
public:
void preOrder(TreeNode* node, vector<int> &arr){
if (!node){
return;
}
preOrder(node->left, arr);
preOrder(node->right, arr);
arr.push_back(node->val);// 后序在递归左右后操作
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if (!root){
return res;
}
preOrder(root, res);
return res;
}
};
非递归实现二叉树遍历
- 先序遍历
先将头结点入栈
- 从栈中弹出一个节点cur
- 打印(处理)cur
- cur(如果有子节点)先右后左入栈
- 重复1-3
递归将每个头结点插入栈中,每次入栈先右后左,保证出栈时是先左后右
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (!root) {
return res;
}
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty())
{
root = stk.top();
stk.pop();
res.push_back(root->val);
if (root -> right != nullptr) {
stk.push(root -> right);
}
if (root->left != nullptr) {
stk.push(root -> left);
}
}
return res;
}
};
- 后序遍历
上述过程实现了先序(头左右),假如子节点入栈顺序改为先左后右,那么出栈变为(头右左),将打印操作变为再压入一个收集栈中,那么最后依次打印收集栈的元素顺序则为(左右头),即为后序遍历
//后序
class postSolution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if (!root) {
return res;
}
stack<TreeNode*> stk;
stack<int> help;
stk.push(root);
while (!stk.empty()) {
root = stk.top();
stk.pop();
help.push(root->val);
if (root->left != nullptr) {
stk.push(root->left);
}
if (root->right != nullptr) {
stk.push(root->right);
}
}
while (!help.empty()) {
res.push_back(help.top());
help.pop();
}
return res;
}
};
- 中序遍历
- 将树的左节点全部入栈
- 没有左节点后,开始弹出栈顶元素,弹出时打印该元素
- 查看弹出元素是否有右节点,有右节点则重复将其右节点的所有左节点入栈,没有右节点则继续弹出栈顶元素。
先将树的左节点入栈,每次弹出时(打印元素)查看弹出节点是否有右节点,如果有右节点将右节点入栈
class Solution {
public:
void inorder(TreeNode* node, vector<int>& arr) {
if (!node) {
return;
}
inorder(node->left, arr);
arr.push_back(node->val);
inorder(node->right, arr);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if (root != nullptr) {
stack<TreeNode*> stk;
TreeNode* cur = root;
while (!stk.empty() || cur != nullptr) {
if (cur) {
stk.push(cur);
cur = cur -> left;
}
else
{
cur = stk.top();
stk.pop();
res.push_back(cur -> val);
cur = cur->right;
}
}
}
return res;
}
};
中序遍历也是二叉树的深度优先遍历
!
二叉树的宽度优先遍历(求树宽)
- 每个节点先左后右入队
- 弹出时打印
- 重复先左后右入队
如果要统计树宽,那么需要知道每个节点所在的层数(用哈希表存储),需要统计每层的节点数目,需要保留宽度最大的层的宽度。
如果不用哈希表:
外层循环对下一层先左后右入队时可以记录下最后一个入队的元素(nextend 先赋值为2再赋值为3),同时我们也可以知道当前层的最后一个元素(head,1)。如果到了curend,那么结算一下当前层的节点数,判断是否需要替换当前记录的最大宽度,将curend替换为nextend,nextend标空。
判断二叉树类型
搜索二叉树
每棵树的节点都是左节点比头节点小,右节点比头结点大称为
搜索二叉树
。
采用中序遍历,如果遍历结果是升序,那么他就是搜索二叉树。判断升序可以记录下遍历过程中相邻的两个值,如果后一个一直大于前一个,则是升序。
完全二叉树
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为
完全二叉树
。
采用宽度优先遍历,头结点入队,从头结点开始每弹出一个结点,将他的节点先左后右入队列。
- 如果遍历过程中出现某个节点只有右节点没有左节点,那么不是完全二叉树
- 在1不违规的情况下,如果遍历过程中,第一次出现节点左右孩子不全的情况时,那么后边的节点必须都是叶子节点(左右节点都为空)
满二叉树
最后一层都是叶子节点,其它层节点都有左右子节点。满二叉树的深度L与总节点个数N之间满足关系:
N = 2^(L-1)
平衡二叉树
所有的子树左子树和右子树之间的高度差≤1是
平衡二叉树
- 采用递归的方式,判断是否平衡二叉树的条件是左子树是平衡二叉树,右子树是平衡二叉树,左子树与右子树的高度差≤1。
- 因此对于每个节点,我们需要询问左(右)子树是否平衡,高度多少。将要询问的内容封装为一个返回类,ReturnType,递归的过程,先询问头结点,然后询问左右子节点,根据返回的信息判断是否是平衡二叉树。
- 递归的出口在于叶子节点没有子树,返回是平衡,高度为0。根据左右子树的高度计算本身数的高度为左右子树高度的较大值加上自身所在层。
int height = Math.max(leftData.height, rightData.height) + 1;
根据左右子树的高度以及是否平衡判断本身是否是平衡二叉树:
boolean isBalanced = leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height - rightData.height) < 2
class Solution {
public:
class callInfo{
public:
callInfo(int height, bool ifBalance){
this->height = height;
this->ifBalance = ifBalance;
}
int height;
bool ifBalance;
};
callInfo isBST(TreeNode* node){
if(node == nullptr){
return callInfo(0, true);
}
callInfo leftInfo = isBST(node -> left);
callInfo rightInfo = isBST(node -> right);
bool ifBalance = leftInfo.ifBalance && rightInfo.ifBalance && (abs(leftInfo.height - rightInfo.height) < 2);
int height = rightInfo.height > leftInfo.height ? (rightInfo.height + 1) : (leftInfo.height + 1);
return callInfo(height, ifBalance);
}
bool isBalanced(TreeNode* root) {
return isBST(root).ifBalance;
}
};
二叉树递归的模板
上边判断是否平衡二叉树思路,即递归向左右子树询问信息的过程可以延展为一种模板解题思路,应用于树形DP问题。
返回两树节点最低公共祖先节点
- 比较直观的解法是先用哈希表记录所有节点的父节点,让节点可以自下向上遍历。节点1,向上遍历直至到根节点,记录遍历路径到hashset中,节点2向上遍历,询问所遍历的节点是否存在于节点1遍历出的hashset中,第一次发现存在的节点即为公共节点。
class Solution {
public:
void traverse(TreeNode* node, unordered_map<TreeNode*, TreeNode*>& m){
if(!node){
return;
}
if(node->left != NULL){
m[node->left] = node;
}
if(node->right != NULL){
m[node->right] = node;
}
traverse(node->left, m);
traverse(node->right, m);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
unordered_map<TreeNode* , TreeNode*> m;
m[root] = root;
traverse(root, m);
set<TreeNode*> s;
TreeNode* cur = p;
while(m[cur] != cur){
s.insert(cur);
cur = m[cur];
}
s.insert(root);
cur = q;
while(m[cur] != cur){
if (s.find(cur) != s.end()){
return cur;
}
cur = m[cur];
}
return root;
}
};
- 代码量更简洁的解法是从根节点开始向左右子树要这两个节点o1和o2,如果有o1和o2的其中之一则返回o1或者o2,如果没有返回null,如果某个节点能从左右子树获取到o1和o2,那么该节点即为所求,返回。
后继节点
中序遍历顺序中处在节点后边的节点称为该节点的后继节点
中序遍历中处在节点前边的节点称为该节点的前驱节点
求节点X的后继节点:
- 当X有右树的时候,X的后继节点是其右树上最左边的节点
- 当X没有右树的时候,那么X是一个最右边的节点,从X向父节点搜索,第一个将X视为左孩子的节点就是X的后继节点。(由于整个树的最右边的节点是没有后继节点的,因此要排除)
无右树时,向上寻找,直至发现第一个父节点将当前节点视为左树,返回父节点。或者一直向上直至父节点为空(根节点),返回父节点,对应没有后继节点的情况。
二叉树的系列化和反序列化
先序序列化:
折纸问题
图
表达图的方式很多,不同的表达方式下的算法不同,可以选择一种表达方式A,将给的表达方式X转为选择的表达方式A来表达,即转变表达方式,不改变算法应用。
邻接表法
记录每个点相邻的点,也可以表达距离的权重。