位运算
位运算规则
- 异或可以理解为不进位加法
>>
右移运算符- 对于无符号数,左边补0
- 对于有符号数,左边补符号位的数
- 任何数与
00...00
异或都等于自身- A^0=A
- 任何数与
111..11
异或等同按位取反。 - 任何数与自身异或都等于0
- A^A=0
位运算作用
- 判断奇偶
x & 1
结果是1则是奇数,为0则是偶数
- 获取二进制位是1还是0
- 用对应二进制位1,其余位0的数与之相与,再右移到最低位与1取余即可
- 交换两个整数变量的值
- 三次异或
a=a^b;b=a^b;a=a^b;
- 不用判断语句,求整数的绝对值
- 负数位运算求绝对值原理:
- 负数补码转原码:除符号位,各位取反,然后+1
- 负数原码取绝对值: 符号位取反
- 因此综合这两步,补码形式的负数要想得到绝对值,即对所有位取反,然后+1即可。
- 至于正数,由于正数的补码和原码相同,保持不变即是自身的绝对值。
- 这里举例子:
- a为正数
- 如果a=2,那么m=0;
- 因为任何数与0异或都等于自身,a=a^m,则a不变。
- a=a-m继续保持不变。
- a为负数
- 如果a=-2,那么m=111…11(有符号数右移左边补符号位);
- 因为任何数和
11.11
异或等同按位取反,所以a=a^m,即所有位置按位取反 - 最后一步需要+1,因为m=
111..11
,是补码形式,值为-1。所以+1即-m;
- a为正数
- 负数位运算求绝对值原理:
int myAbs(int a){
int m=a>>31;
a=a^m;
a=a-m;
return a;
}
第一题
异或有去重的作用
原理:A^A=0; B^0=B
解题思路:将序列元素全部异或,然后再和1~1000进行异或,成对的数都变成了0,任何数与0异或又等于自身,所以结果就剩下了重复的数。
#include <cstdio>
using namespace std;
int main() {
int arr[1001];
for (int i = 1; i <= 1000; i++) {
arr[i - 1] = i;
}
arr[1000] = 3; //重复元素是3
int res = 0;
//和序列元素全部异或
for (int i = 0; i < 1001; i++)
res ^= arr[i];
//与1到1000异或
for (int n = 1; n <= 1000; n++)
res ^= n;
printf("%d", res);
return 0;
}
注意初始res要设置为0
第二题
方法同上,异或去重
第三题
题意应该是正整数
方法一
每次取最低位,如果是1,count++,然后右移一位
#include <cstdio>
using namespace std;
int main() {
int n;
//假设输入的都是正整数
scanf("%d",&n);
int count=0;
while(n){
if(n&1){
count++;
}
n>>=1;
}
printf("Num of binary '1' : %d",count);
return 0;
}
方法二 ⭐
使用**-1再与自身相与**的方法来消除尾1,计算一共消除多少次尾1即可
#include <cstdio>
using namespace std;
int main() {
int a;
scanf("%d",&a);
int count=0;
while(a){
a&=(a-1);
count++;
}
printf("%d",count);
return 0;
}
第四题
同上,判断是否只出现一个1。
#include <cstdio>
bool isTwoMultiple(int n) {
if (!n)
return false;
return !(n & (n - 1));
}
int main() {
int N;
scanf("%d", &N);
if (isTwoMultiple(N))
printf("Yes");
else
printf("No");
return 0;
}
第五题
题目:将整数的奇偶位互换
(题意是第一位和第二位换,第三位和第四位换…)
思路:用101010…和010101…把奇数位和偶数位抠出来
然后一个左移一位,一个右移一位,然后进行或运算
#include <cstdio>
using namespace std;
void moveOddAndEven(int &N){
int odd=0xaaaaaaaa;
// int odd=0b10101010101010101010101010101010;
int even=0x55555555;
// int even=0b01010101010101010101010101010101;
N=((N&odd)>>1)^((N&even)<<1);
}
int main() {
int N;
scanf("%d",&N);
moveOddAndEven(N);
printf("%d",N);
return 0;
}
第六题
//
// Created by 14259 on 2021/5/2.
//
#include <cstdio>
#include <string>
#include <iostream>
using namespace std;
void printBinaryForm(double N){
string res="0.";
for(int i=0;i<32;i++){
N*=2;
if(N>=1){
N-=1;
res+='1';
}
else{
res+='0';
}
if(N==0){
cout<<res;
return;
}
}
printf("ERROR");
}
int main() {
double N;
scanf("%lf",&N);
printBinaryForm(N);
return 0;
}
第七题
思路:将所有数转换为k进制,然后做不进位加法(只有二进制的不进位加法才是异或运算),最后结果转换为10进制即可。
原理:
- 2个相同的2进制数做不进位加法(二进制不进位加法即异或),结果为0
- 10个相同的10进制数做不进位加法,结果为0
- k个相同的k进制数做不进位加法,结果为0
(因为每个位上k个数相加,一定是k的倍数,若是k进制,则每位一定为0)
注意定义结构体的时候,用字符串数组,或者vector<string>来记录一个数,不要直接用字符串,因为一个位上超过10,会产生歧义。
查找与排序
递归
我总结的递归三要素:
- 递归终止条件
- 如何向递归终止条件演进
- 搞清楚递归函数的含义(即递归函数做了一件什么事)
递归数组求和
#include <cstdio>
#include <iostream>
using namespace std;
int calSum(int *arr,int left,int right){
if(left==right)
return arr[left];
return arr[left]+calSum(arr,left+1,right);
}
int main() {
int arr[]={1,2,3,4,5};
cout<<calSum(arr,0,4);
return 0;
}
汉诺塔问题
此问题的关在在于,搞清楚递归函数的含义
#include <cstdio>
#include <iostream>
using namespace std;
//辗转相除法求最大公因数
int gcd(int a,int b){
if(b==0)
return a;
return gcd(b,a%b);
}
/**
* 递归的含义是:将src上的N个盘子,移动到des上,assist(辅助必须盘子为空)作为辅助
* @param N
*/
void Hanoi(int N,char src,char des,char assist){
//边界条件
if(N==1){
printf("%c --> %c\n",src,des);
return;
}
if(N==2){
printf("%c --> %c\n",src,assist);
printf("%c --> %c\n",src,des);
printf("%c --> %c\n",assist,des);
return;
}
//递归演进
Hanoi(N-1,src,assist,des);
Hanoi(1,src,des,assist);
Hanoi(N-1,assist,des,src);
}
int main() {
Hanoi(3,'a','b','c');
return 0;
}
排序
简单插入排序
void insertSort(int *arr,int len){
for(int i=1;i<len;i++){
int j=i;
while(j>=1 && arr[j]<arr[j-1]){
swap(arr[j],arr[j-1]);
j--;
}
}
}
时间复杂度:O(N2)
进一步优化:每一轮寻找当前元素的最终位置时,可以使用二分查找,时间复杂度降为O(nlogn)
希尔排序
希尔排序可以理解为分组插入排序,是插入排序的一种改进。
因为插入排序要进行大量无效的比较(每一轮要将一个元素往前送,不停地比较),而希尔排序能够以较大的步伐将小元素往前送,大元素往后摆,这样大大减少了原来插入排序所需要比较的次数,从而提高了速度。
(希尔排序的时间复杂度涉及到数学上尚未解决的难题)
void shellSort(int *arr,int len){
for(int interval=len/2;interval>=1;interval/=2){
//对每个分组进行插入排序
//i是每组的开头元素位置
for(int begin=0;begin<interval;begin++){
//以interval作为间隔的一组数,进行插入排序
for(int i=begin+interval;i<len;i+=interval){
int j=i;
while(j-interval>=0 && arr[j]<arr[j-interval]){
swap(arr[j],arr[j-interval]);
j-=interval;
}
}
}
}
}
评估算法性能
- 评估算法性能,主要评估问题的输入规模n与元素的访问次数f(n)之间的关系
- 大O符号,忽略非主体部分,如常数项、低阶项
1s不同复杂度能处理的规模
- n : 108
- n2 : 104
- n3 : 500以下
- 2n : 27以下
- logn : 2^(108) ⭐
各种复杂度的常见算法
- O(n2) :冒泡排序,直接插入排序,选择排序
- O(nlogn) :归并排序,快速排序
三种典型递归算法的性能分析
-
数组求和:递归解法
复杂度是O(n),即O(1)*n,子问题规模下降层数为n,每个子问题的答案消耗的时间为O(1)。
-
斐波那契数列:
f(n)=f(n-1)+f(n-2),复杂度是O(2n)
因为递归形式是个二叉树(高度约为n,节点个数为2n左右),子问题答案求解消耗的时间O(1) -
汉诺塔问题
和斐波那契数列类似,O(2n) -
最大公约数
时间复杂度O(logn)
排序算法稳定性
稳定:两个相同的数,经过排序,前后相对位置(谁在前,谁在后)不会发生变化
几种不稳定的排序算法
- 希尔排序:不稳定
- 因为相同元素可能分到不同组中,不同组内调换顺序,可能导致不稳定
- 选择排序:不稳定
- 因为涉及到交换,会把当前数字换到后边
- 例如:
3,3,1
,此时最小元素是1,会将1与第一个3交换,导致不稳定
- 堆排序:不稳定
- 快速排序:不稳定
剩下的算法都稳定:插入排序、冒泡排序、归并排序、基数排序、计数排序、桶排序。
算法稳定的好处
比如这么一个场景:有一批订单,按日期降序排列,如果日期一样的,按订单金额降序。
这个需要很好理解,实现起来,可能就没那么容易,按日期排好了,再把日期一样的按订单金额排一次。这个就不好实现,先把日期一样的分别取出来排下,再对应放回去,很麻烦。
有了稳定性排序算法,就很容易实现。第一步,先按订单金额降序排列,第二步,再把第一步得到的结果,按日期降序重新排列一次,就可以实现需求了。
————————————————
此案例转载自CSDN博主「北枫凉」的一篇文章
原文链接:https://blog.csdn.net/qq_34686440/article/details/105112263
例题1
/**
* 递归函数含义:上n阶台阶有多少种走法
* @param n
* @return
*/
int upstairs(int n){
//边界条件
if(n<0)
return 0;
if(n==0)
return 1;
//递归演进
return upstairs(n-1)+upstairs(n-2)+upstairs(n-3);
//类似背包问题,假设已经装了部分,对剩下部分的处理,用递归来解决
}
例题2
注意体会high = pivot
和low = pivot + 1
还有high -= 1
最终目的是使left==right
的时候,刚好是最终答案。
int minArray(vector<int>& numbers) {
int low = 0;
int high = numbers.size() - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (numbers[pivot] < numbers[high]) {
high = pivot; //nums[pivot]可能是答案
}
else if (numbers[pivot] > numbers[high]) {
low = pivot + 1; //nums[pivot]不可能是答案
}
else {
high -= 1;
}
}
return numbers[low];
}
例题3
写这一题的体会在于:处理特殊情况时,如果情况复杂,采用分类讨论。如果有类似两端都有情况时,选取一端作为分类的标准比较明智(多次遇到这种情况了)
//
// Created by 14259 on 2021/5/4.
//
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
int findPos(string arr[], int len,string target) {
int l = 0, r = len - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
int t=mid;
while (t <= r && arr[t]==""){
t++;
}
if(t>r){
r=mid-1;
continue;
}
if(arr[t]==target){
return t;
}
if(target<arr[t]){
r=mid-1;
}
else
l=t+1;
}
return -1;
}
int main() {
string arr[] = {"a", "", "ac", "ad", "b", "", "ba"};
int len = sizeof(arr) / sizeof(*arr);
cout<<findPos(arr,len,"abc");
return 0;
}
例题4
写一个递归形式的直接插入排序
//
// Created by 14259 on 2021/5/4.
//
#include <cstdio>
#include <algorithm>
using namespace std;
//首先要知道:插入排序的思想是将序列分为已经排序和未排序的部分
/**
* left及其左侧是已经排序好了的
* @param arr
* @param left
* @param right
*/
void insertSort(int arr[],int left,int right){
//递归终止条件
if(left==right)
return;
//处理当前问题
int t=left+1;
while(t>0 && arr[t]<arr[t-1]){
swap(arr[t],arr[t-1]);
t--;
}
//处理子问题
insertSort(arr,left+1,right);
}
int main(){
int arr[]={0,0,-1,-2,3,3,-1,4,55,9,0};
int len=sizeof(arr)/sizeof(*arr);
insertSort(arr,0,len-1);
for(int i=0;i<len;i++){
printf("%d ",arr[i]);
}
return 0;
}
递归的关键在于:当前问题可以拆解为更小的类似的问题。
例题5
快速幂