排序
选择排序
void select_sort()
{
for(int i=1;i<=n;i++)
{
int k=i;
for(int j=i;j<=n;j++) //选出[i,n]中最小的元素,下标为k
{
if(A[j]<A[k])
{
k=j;
}
}
//交换A[i]和A[k]
int temp = A[i];
A[i] = A[k];
A[k] = A[i];
}
}
插入排序
int A[maxn],n;
//A数组下标 从1~n
void insert_sort()
{
for(int i = 2;i<=n;i++)
{
int temp = A[i], j = i;
//只要temp小于前一个元素A[j-1],把A[j-1]后移一位至A[j]
while(j>1 && temp < A[j-1])
{
A[j] = A[j-1];
j--;
}
A[j] = temp; //插入位置为j
}
}
sort()函数的使用
比较函数cmp
基本数据类型排序
#include<cstdio>
#include<algorithm>
using namespace std;
int main()
{
int a[5] = {
3,1,4,2};
sort(a,a+4);
for(int i=0;i<4;i++)
{
printf("% ",a[i]);
}
return 0;
}
输出结果:1 2 3 4
如果想要反向输出可以写一个比较函数,如下:
bool cmp(int a,int b)
{
return a>b; //可以理解为a>b时把a放前面
} //也可以认为是左大右小
这样之后就会输出4 3 2 1
结构体数组排序
#include<stdio.h>
#include<algorithm>
struct node{
int x,y;
}ssd[10];
bool cmp(node a,node b)
{
return a.x>b.x;
}
int mai()
{
ssd[0].x = 2;
ssd[0].y = 2;
ssd[1].x = 1;
ssd[1].y = 3;
sort(ssd,ssd+2,cmp);
for(int i=0;i<2;i++)
{
printf("%d %d",ssd[i].x,ssd[i].y);
}
return 0;
}
容器的排序
例题
strcmp() 函数:
int strcmp(const char *str1, const char *str2)
**This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.**也就是一个字符一个字符的比较ASCII码
如果返回值小于 0,则表示 str1 小于 str2。
如果返回值大于 0,则表示 str1 大于 str2。
如果返回值等于 0,则表示 str1 等于 str2。
当两个字符串不相等时,C 标准没有规定返回值会是 1 或 -1,只规定了正数和负数
注意:\0的ASCII是0
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct Student{
char id[15];
int score;
int location_number;
int local_rank;
}stu[30010];
bool cmp(Student a,Student b)
{
if(a.score != b.score) return a.score>b.score; //按照分数从高到低排序
else return strcmp(a.id,b.id)<0; //分数相同按准考证号从小到大排序
}
int main()
{
int n,k,num=0; //num是总的考生数
scanf("%d",&n); // n是考场数
for(int i=1;i<=n;i++)
{
scanf("%d",&k); //这个考场中的人数
for(int j=0;j<k;j++)
{
scanf("%s %d",stu[num].id,&stu[num].score);
stu[num].location_number = i;
num++;
}
//下面将这个考场中的考生进行排序
sort(stu + num -k,stu + num,cmp);
stu[num-k].local_rank = 1; //这个考场第一名的等级记为1
for(int j = num - k+1;j<num;j++)
{
if(stu[j].score == stu[j-1].score)
stu[j].local_rank = stu[j-1].local_rank;
else//否则排名等于这个考生之前的人数加一
stu[j].local_rank = j - (num-k) + 1;
}
}
printf("%d",num);
sort(stu,stu+num,cmp);
int r=1; //当前考生的排名
for(int i=0;i<num;i++)
{
if(i>0&&stu[i].score != stu[i-1].score)
{
r = i+1; //r更新为人数
}
printf("%s ",stu[i].id);
printf("%d %d %d\n",r,stu[i].location_number,stu[i].local_rank);
}
return 0;
}
散列
改进后的时间复杂度是 O( M+N )
#include<cstdio>
const int maxn = 100010;
bool hashTable[maxn] = {
false};
int main()
{
int n,m,x;
scanf("%d%d",&n,&m);
for(int i = 0;i<n;i++)
{
scanf("%d",&x);
hashTable[x] = true;
}
for(int i = 0;i<m;i++)
{
scanf("%d",&x);
if(hashTable[x])
{
printf("Yes\n");
}
else
{
printf("No\n");
}
}
return 0;
}
上面的方法采用了空间换取时间的方法:直接把输入的数作为数组的下标来对这个数的性质进行操作
由此,引入了散列的概念
将元素通过一个函数,转化为整数,使得该整数可以尽量唯一的表示这个元素, 其中把用于转化的这个函数称为散列函数 H , 如果记转化前的元素名叫做key, 那么转化后的元素就是一个整数 H(key)
线性探查法
平方探查法
链地址法
负载因子
字符串hash初步
int hashFunc(char S[],int len)
{
int id=0;
for(int i=0;i<len;i++)
{
id = id*26 + (S[i]-'A'); //S[i]-'A' 将字符对应到0-25, 那个id*26,可以理解为把一个值扩大,这样就会使得一个字符串有一个较大的并且不太可能和其他字符串一样的整数id ,
//所以,如果len比较大,那么转化的字符串对应的id就会很大很大,所以如果这么来转化len不能太大
}
return id;
}
下面是出现小写字母的情况,我们把A~Z作为0 ~ 25 , 把a ~ z 作为 26 ~ 51
int hashFunc(char S[],int len)
{
int id=0;
for(int i=0;i<len;i++)
{
if(S[i]>= 'A' && S[i] <= 'Z')
{
id = id*52 + (S[i]-'A');
}
else if(S[i]>='a' && S[i]<='z')
{
id = id*52 + (S[i]-'a');
}
}
return id;
}
#include<cstdio>
const int maxn = 100;
char S[maxn][5], temp[5];
int hashTable(26*26*26 + 10);
int hashFunc(char S[],int len)
{
int id = 0;
for(int i=0;i<len;i++)
{
id = id*26+(S[i]-'A');
}
return id;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
{
scanf("%s",S[i]);
int id = hashFunc(S[i],3);
hashTable[id]++; //字符串的出现次数加一
}
for(int i=0;i<m;i++)
{
scanf("%s",temp);
int id=hashFunc(temp,3);
printf("%d\n",hashTable[id]);
}
return 0;
}
递归
递归就在于反复调用自身函数,但是每一次调用把范围减少,直到范围缩小到可以直接获得范围边界结果,然后再在返回路上求出对应解
递归逻辑中最重要的两个概念:
- 递归边界:分解的尽头
- 递归式(递归调用):把问题分解为若干个子问题的手段
实例:使用递归求阶乘
int F(int n)
{
if(n==0) return 1;
else return F(n-1)*n;
}
实例:使用递归求斐波那契数列
int F(int n)
{
if(n==0 || n==1) return 1;
else return F(n-1)+F(n-2);
}
全排列
#include<cstdio>
const int maxn = 11;
//p为当前的排列,hashTable记录整数x是否已经在p中
int n,P[maxn],hashTable[maxn] = {
false};
//当前处理排列的第index位
void generateP(int index)
{
if(index == n+1)//递归边界,已经处理完排列的1~n位
{
for(int i=1;i<n;i++)
{
printf("%d",P[i]); //输出当前的排列
}
printf("\n");
return; // return; 相当于break
}
for(int x=1;x<=n;x++) //枚举1~n,试图将n填入 P[index]
{
if(hashTable[x]==false)
{
P[index] = x;
hashTable[x] = true;
generateP(index+1);
hashTable[x] = false;
}
}
}
int main()
{
n = 3;
generateP(1);
return 0;
}
递归可以理解为就是一个函数,出现了若干个分治,这若干个分治,又会进行分支,直到遇到问题边界,一个分支结束,输出这个分支的结果
n皇后问题
#include<cstdio>
#include<cmath>
int count = 0;
int n;
//P[i]是列,i是行
int P[20],hashTable[20] = {
false};
void generateP(int index)
{
if(index == n+1)//递归边界,已经处理完排列的1~n位
{
bool flag = true; //flag为true表示当前排列是一个合法的方案
for(int i=1;i<n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(abs(i-j)==abs(P[i]-P[j])) flag = false;
}
}
if(flag) count++;
return;
}
for(int x=1;x<=n;x++)
{
if(hashTable[x]==false)
{
P[index] = x; //index相当于行数,给每一行选择一个不同的列,就相当于一个全排列的问题
hashTable[x] = true;
generateP(index+1);
hashTable[x] = false;
}
}
}
int main()
{
n = 3;
generateP(1);
return 0;
}
void generateP(int index)
{
if(index == n+1)
{
count++; //能够到达这个地方的分支一定是合法的
return;
}
//index表示行
for(int x=1;x<=n;x++) //第x列
{
if(hashTable[x]==false) //第x列还没有皇后
{
bool flag = true; //flag为true表示当前的皇后不会与之前的皇后发生冲突
for(int pre=1;pre<index;pre++) //遍历之前的皇后
{
if(abs(index-pre)==abs(x-P[pre]))
{
flag = false;
break;
}
}
if(flag)
{
P[index] = x;
hashTable[x] = true;
generatep(index+1);
hashTable[x] = false;
}
}
}
}
贪心法
例题一:月饼
#include<cstdio>
#include<algorithm>
using namespace std;
struct mooncake
{
double store; //库存量
double sell; //总售价
double price; //单价
}cake[1010];
bool cmp(mooncake a,mooncake b)
{
return a.price>b.price; //按照单价从高到低进行排序
}
int main()
{
int n; //月饼的种类
double D; //月饼的需求量,为了后边的计算,声明了D是double类型
scanf("%d%lf",&n,&D);
for(int i=0;i<n;i++)
{
scanf("%lf",&cake[i].store);
}
for(int i=0;i<n;i++)
{
scanf("%lf",&cake[i].sell);
cake[i].price = cake[i].sell/cake[i].store;
}
sort(cake,cake+n,cmp);
double ans = 0;
for(int i=0;i<n;i++)
{
if(cake[i].store<=D) //如果需求量高于月饼的库存量,这里已经将月饼的单价按照从高到低排序了
{
D -= cake[i].store;
ans += cake[i].sell;
}
else //库存量大于需求量
{
ans+=cake[i].price * D;
break;
}
}
printf("%.2f\n",ans);
return 0;
}
上面的例子就是一个贪心算法的实例,每次我总是选择单价最高的那个月饼卖出
例题二:组个最小数
#include<cstdio>
int main()
{
int count[10]; // 记录数字0~9的个数
for(int i=0;i<10;i++)
{
scanf("%d",&count[i]);
}
for(int i=1;i<10;i++)
{
if(count[i]>0) //从1~9中选择count 不为0的最小的数字,作为第一位
{
printf("%d",i);
count[i]--;
break;
}
}
for(int i=0;i<10;i++)
{
for(int j=0;j<count[i];j++)
{
printf("%d",i);
}
}
return 0;
}
区间不相交问题
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 110;
struct Interval{
int x,y;
}I[maxn];
bool cmp(Interval a, Interval b)
{
if(a.x!=b.x) return a.x>b.x;
else return a.y<b.y;
}
int main()
{
int n;
while(scanf("%d",&n),n!=0)
{
for(int i=0;i<n;i++)
{
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);
int ans=1,lastX = I[0].x;
for(int i=1;i<n;i++)
{
if(I[i].y<lastX)
{
lastX = I[i].x;
ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
二分
二分查找
二 分 查 找 的 时 间 复 杂 度 是 O ( l o g n ) \color{#FF0000}{二分查找的时间复杂度是 O(logn)} 二分查找的时间复杂度是O(logn)
二分查找用于在一个严格递增或者递减的序列A中找到数x
下面是递增的写法:
int binarySearch(int A[],int left, int right,int x)
{
int mid;
while(left<=right)
{
mid = (left+right)/2;
if(A[mid]==x) return mid;
else if(A[mid]>x){
right = mid-1;
}
else{
left = mid+1;
}
}
return -1;
}
问题一:求出序列中第一个大于等于x的元素的位置L
int lower_bound(int A[],int left, int right,int x)
{
int mid;
while(left<right)
{
mid = (left+right)/2;
if(A[mid]>=x) right = mid;
else{
left = mid+1;
}
}
return left;
}
问题二:寻找第一个大于x的数的位置
int lower_bound(int A[],int left, int right,int x)
{
int mid;
while(left<right)
{
mid = (left+right)/2;
if(A[mid]>x) right = mid;
else{
left = mid+1;
}
}
return left;
}
快速幂
Two pointers思想
就是利用两个下标进行扫描,这样复杂度一般情况下为O(n)
序列合并问题
A,B是两个递增的序列,现在来考虑把他们合并成一个序列C(当然C也是递增的)
int merge(int A[],int B[],int C[],int n,int m)
{
//n,m分别是数组A和B的长度
int i=0,j=0,index=0; //i指向A[0],j指向B[0]
while(i<n&&j<m)
{
// ++i 是先加后赋值;i++ 是先赋值后加
if(A[n]<= B[m])
{
C[index++] = A[i++];
}
else{
C[index++] = B[j++];
}
}
while(i<n) C[index++] = A[i++]; //如果A数组有剩余,把剩下的数加进去
while(j<m) C[index++] = B[j++]; //如果B数组有剩余,把剩下的数加进去
return index; //返回得到的数组C的长度
}
归并排序
下面是2路归并算法实现
2路归并递归算法
const int maxn = 100;
//merge函数,将数组A的[L1,R1]与[L2,R2]区间合并成一个有序区间
void merge(int A,int L1,int R1,int L2,int R2)
{
int i=L1,j = L2;
int temp[maxn],index = 0; //temp数组用来暂时存放合并数组
while(i<=R1 && j<=R2)
{
if(A[i]<= A[j])
{
temp[index++] = A[i++];
}
else temp[index++] = A[j++];
}
while(i<=R1) temp[index++] = A[i++];
while(j<=R2) temp[index++] = A[j++];
for(i = 0;i<index;i++)
{
A[L1+i] = temp[i];
}
}
//将array数组的区间[Left,right]进行归并排序
void mergeSort(int A[],int left,int right)
{
if(left<right)
{
int mid = (left+right)/2;
mergeSort(A,left,mid);
mergeSort(A,mid+1,right);
merge(A,left,mid,mid+1,right);
}
}
2路归并非递归算法
const int maxn = 100;
//将数组A的[L1, R1]与[L2, R2]区间合并为有序区间(此处L2记为R1 + 1)
void merge(int A[], int L1, int R1, int L2, int R2) {
int i = L1, j = L2; //i指向A[L1], j指向A[L2]
int temp[maxn], index = 0; //temp临时存放合并后的数组,index为其下标
while(i <= R1 && j <= R2) {
if(A[i] <= A[j]) {
//如果A[i] <= A[j]
temp[index++] = A[i++]; //将A[i]加入序列temp
} else {
//如果A[i] > A[j]
temp[index++] = A[j++];//将A[j]加入序列temp
}
}
while(i <= R1) temp[index++] = A[i++]; //将[L1, R1]的剩余元素加入序列temp
while(j <= R2) temp[index++] = A[j++]; //将[L2, R2]的剩余元素加入序列temp
for(i = 0; i < index; i++) {
A[L1 + i] = temp[i]; //将合并后的序列赋值回数组
}
}
void mergeSort(int A[]) {
//step为组内元素个数, step / 2为左子区间元素个数,注意等号可以不取
//n为元素的个数
for(int step = 2; step / 2 <= n; step *= 2) {
//每step个元素一组,组内前step / 2和后step / 2个元素进行合并
for(int i = 1; i <= n; i += step) {
//对每一组
int mid = i + step / 2 - 1; //左子区间元素个数为step / 2
if(mid + 1 <= n) {
//右子区间存在元素则合并
//左子区间为[i, mid],右子区间为[mid+1, min(i+step-1, n)]
merge(A, i, mid, mid + 1, min(i + step - 1, n));
}
}
}
}
//使用sort函数代替merge函数
void mergeSort(int A[]) {
//step为组内元素个数,step / 2为左子区间个数,注意等号可以不取
for(int step = 2; step / 2 <= n; step *= 2) {
//每step个元素一组,组内[i, min(i+step, n+ 1)]进行排序
for(int i = 1; i <= n; i += step) {
sotr(A + i, A + min(i + step, n + 1));
}
//此处可以输出归并排序的某一趟结束的序列
}
}
快速排序
-
算法思想
从待排序序列中选取一个元素,以其值作为中间值,把比其小的元素放到左边,比起大的元素放到右边;然后递归地对左、右部分排序,直至每一部分元素个数为1,整个序列有序。 通过一趟排序将要排序的数据分割成两个独立的部分,以选取的关键字为分界线(关键字一般选取第一个要排序的元素)。其中一部分的所有数据都比另外一部分的所有数据都要小(以关键字为分界线),然后再按此方法对这两部分数据分别进行快速排序,整个排序过程递归进行,以此达到整个数据变成有序序列。
-
时间复杂度
用递归树的思想,每次划分操作的元素总数都是n,因此每次划分的时间复杂度都是O(n),接下来只需考虑划分次数: 最好情况 O(nlogn):每次都将序列等分为两部分,因此划分次数为logn 最坏情况 O(n^2):每次都有一部分只分出一个元素,因此划分次数为n
-
空间复杂度 O(logn)
递归调用,考虑递归栈的空间消耗
-
稳定性
**不稳定**。划分时要跨中轴交换元素,不相邻交换不稳定
快速排序过程:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i++或j–完成的时候,此时令循环结束)。
#include<iostream>
#include<cstdlib>
#define MAXSIZE 7
using namespace std;
typedef struct
{
int r[MAXSIZE+1];
int length;
}SqList;
//交换函数
void swap(SqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=temp;
}
//将比枢轴pivotkey小的元素放到左边,大的放到右面,然后将pivotkey放入合适的位置
int Partition(SqList *L,int low,int high)
{
int pivotkey;
pivotkey=L->r[low]; //将第一个元素作为枢轴pivotkey
while(low<high) //从表的两端交替向中间扫描
{
while(low<high && L->r[high]>=pivotkey) //从右边起,将比pivotkey小的换到左边
high--;
swap(L,low,high);
while(low<high && L->r[low]<=pivotkey) //从左边起,将比pivotkey大的换到右边
low++;
swap(L,low,high);
}
return low; //low与high相等时,即为枢轴位置
}
//递归
void QSort(SqList *L,int low,int high)
{
int pivot;
if(low<high)
{
pivot=Partition(L,low,high); //将表一分为二
QSort(L,low,pivot-1); //低子表继续递归
QSort(L,pivot+1,high); //高子表继续递归
}
}
//初始化函数
int Init(SqList ** L)
{
*L=NULL;
*L=(SqList *)malloc(sizeof(SqList));
if(*L==NULL)
{
return 0;
}
(*L)->length=0;
return 1;
}
//插入函数
void Insert(SqList **L,int data)
{
if(data)
{
(*L)->length++;
(*L)->r[(*L)->length]=data;
}
}
//输出函数
void Printf(SqList *L)
{
int i;
for(i=1;i<=L->length;i++)
{
cout<<L->r[i]<<" ";
}
}
//主函数
int main()
{
SqList *L;
int i;
Init(&L);
int a[MAXSIZE];
cout<<"请输入MAXSIZE大小的元素个数:"<<endl;
for(i=0;i< MAXSIZE;i++)
{
cin>>a[i];
Insert(&L,a[i]);
}
QSort(L,1,L->length);
Printf(L);
}
活用递推
#include<cstdio>
#include<cstring>
const int MAXN = 100010;
const int MOD = 100000007;
char str[MAXN];
int leftNumP[MAXN] = {
0}; //记录每一位左边的p的个数
int main()
{
gets(str);
int len = strlen(str);
for(int i=0li<len;i++)
{
if(i>0){
leftNumP[i] = leftNumP[i-1]; // 继承上一位的p的数目,不可以再次遍历,要不然会超时
}
if(str[i]=='p') leftNumP[i]++;
}
int answer = 0,rightNumT = 0;
for(int i = len - 1;i>=0;i--)
{
if(str[i] == 'T')
{
rightNumT++;
}
else if(str[i]=='A')
{
ans = (ans +leftNumP[i]*rightNumT)%MOD; // 题目中要求说要取余数注意读题。。。
}
}
printf("%d\n",ans);
return 0;
}
随机选择算法
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<algorithm>
using namespace std;
const int maxn = 100010;
int A[maxn],n;
//选取随机主元,对区间[left,right]进行划分
int randPartition(int A[],int left,int right)
{
// 生成[left,right]内的随机数p
int p = (round(1.0*rand())/RAND_MAX*(right-left) + left);
swap(A[p],A[left]);
// 以下为Partition函数划分过程
int temp = A[left];
while(left<right)
{
while(left<right && A[right]>temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
//随机选择算法,从A[left,right]中找到第k大的数,并且,进行切分
void randSelect(int A[],int left,int right,int k)
{
if(left == right) return;
int p = randPartition(A,left,right);
int M = p - left + 1;
if(k == M) return;
if(k<M){
randSelect(A,left,p-1,k);
}
else{
randSelect(A,p+1,right,k-M);
}
}
int main()
{
srand((unsigned)time(NULL));
int sum = 0,sum1 = 0;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&A[i]);
sum+=A[i];
}
randSelect(A,0,n-1,n/2);
for(int i=0;i<n;i++)
{
sum1+=A[i];
}
printf("%d\n",(sum-sum1)-sum1);
return 0;
}