习题课2-1

习题课2-1

排序

解法1 选择排序

  • for(int i=0;i<n;i++){
    	int minEle = a[i];
        int tmp;
    	for(int j=i+1;j<n;j++){
    		minEle = Math.min(minEle,a[j]);
            tmp = j;
    	}
        a[j] = a[i];
    	a[i] = minELe;	
    }
    
  • 选择排序:每次选出一个最小的放在前面

  • 每次都要全部扫一遍,

  • 复杂度一直都是O(n^2)

  • 每次扫描,用一个额外变量记录最小元素的下标

  • 扫描完毕,通过记录的最小元素下标,将初始最小元素和记录的最小元素交换。

解法2 插入排序

  • for(int i=0;i<n;i++)
    	for(int j=i;j>0;j--{
    		if(a.get(j)<a.get(j-1)
    			collections.swap(a,j,j-1);
    		else
    			break;
    	}
    
  • 插入排序:每次将元素插入到合适的位置

  • 最坏的情况是将一个倒序的数组排序为正序,此时就是O(n2)的

  • 保证游标走到位置之前都是有序的,插入一个新元素时,会前缀有序序列的末尾开始往前逐一比较,如果小于就交换,不小了就直接break,因为已经是有序排列的

解法3 冒泡排序

  • for(int i=1;i<n;i++)
    	for(int j=0;j<n-i;j++){
    		if(a.get(j)>a.get(j+1)
    			Collections.swap(a,j,j+1);
    	}
    
  • 冒泡排序:每一次将最大的泡冒到最后,刚好与选择排序相反

  • i从1开始,是防止j+1越界

解法4 归并排序

  • void sort(int l,in r){
    	if(l==r)//只有一个元素了,不需要操作了
    		return;
    		
    	int mid = (l+r)>>1;
    	sort(l,mid);
    	sort(mid+1,r);
    	int l_idx = l;
    	int r_idx = mid+1;
    	int b_idx = l;
    	while(l_idx<=mid && r_idx<=r)
    		if(a(l_idx)<a(r_idx)
    			b[b_idx++] = a[l_idx++];
    		else
    			b[b_idx++] = a[r_idx++];
    	while(l_idx<=mid)
    		b[b_idx++] = a[l_id++];
    	while(r_idx<=r)
    		b[b_idx++] = a[r_idx++];
    	for(int i=l;i<=r;i++)
    		a[i] = b[i];
    }
    
  • 归并排序:递归排序左右两半,再合并

  • 递归的排序(实际是一个后序遍历)

  • 切分的前后两半数量不超过1

  • 第一步是考虑边界情况

  • 将数组切分成两半,找到中间值,递归排序左右两半

  • 再合并排序好的两部分

    • b[]是一个新数组,b_idx是新数组的下标

    • 首先同时从左右两半中取出更小的数放到b数组中,此时始终只会有一个数组全部放到了b数组中

    • 所以之后需要判断左右两个数组的下标,判断是否全部放到b数组中了,如果没有继续往b数组中填充

    • 最后将新排序好的数组重新赋值给原数组a

解法5 快速排序

  • void sort(int l,int r){
    	if(l>=r) //l是向右走的,r是向左走的
    		return;
    	int bound = a[rand(l,r)]; //取一个随机的数
    	int l_idx = l;
    	int r_idx = r;
    	while(l_idx<=r_idx) { //必须有等于,不然左右下标相等就不动了,这样递归下去左右两半就有了重复的部分
            // 下面必须是小于,因为等于和小于的界桩值都放在左边,而此时的如果这两个值是3,2,就没有起到排序的作用,保证了l_idx的左边的数全部都是小于界桩值的
    		while(a[l_idx]<bound) //从左边找到第一个比界桩大的值
    			++l_idx;
    		while(bound<a[r_idx]) //在右边找到第一个比界桩小的值
    			--r_idx;
    		if(l_idx<=r_idx) //与while循环中的等号是一样的,如果没有等号,递归时可能是重复的
    			swap(a[l_idx++],a[r_idx--]);//交换左右两个值的同时,左下标+1,右下标-1
    	}
        // while执行完后,l是大于r的,大1或者大2
    	sort(l,r_idx);
    	sort(l_idx,r);
    }
    
  • 快速排序:随机找一个数作为界值,然后将序列划分成左右两端递归(前序遍历)

  • 一定要注意快速排序的边界情况

  • 找界桩值pivot,使的pivot前面都比pivot小,pivot后面的元素都比pivot大

  • 切分数组,得到一个下标j,此时的j就是第一条所说的pivot,然后对(lo,r_pivot)和(l_pivot,hi)递归排序即可

  • 改进的点在于界桩值一般是取a[0],此处取的随机值,有可能直接选中了界桩值

  • 默认不支持开栈,默认栈空间是8M

补充

堆排序

  • 选择排序用堆这种数据来存储。

拓扑排序

  • 有向带权图(只有一个入度为1的点)

桶排序(基数排序)

  • 哈希表
  • 质数取模

分组

  • 简要题意:n个正整数,分成连续m份,要求最大的那一份的数字之和尽量小。

  • 最大的最小,看到这个关键字,就要想想二分能不能做。

  • 转化题意,保证每份之和不超过d,最少能分成多少份,设这个份数是cnt

  • 如果cnt<=m,并且n>=m的,所以一定是可以划分成m份的,而当cnt>m了,则不能划分成m份

  • 二分答案

  • 主函数

  • // 返回值:最优方案中,数字之和最大的那一份的数字之和
    long long getAnswer(int n,int m,vector<int> a){
        // l表示答案下界,r表示答案上界
    	long long l = 1, r = 0;
    	
        // 求出初始上界r
        // 分成1份,所有的数归成1份
        // 不能有负数,不然这样求出的上界就不是最大的了
    	for(int i=0;i<n;i++){
    		r += a[i];
    	}
    	
        // 二分答案
    	while(l<=r){
    		long long mid = (l+r) >> 1;
            // 验证mid可不可能为答案,如果可能,上界缩小
            // 因为mid满足了,就可以去(l,mid-1)的区间里去找尽可能小的d值
    		if(check(mid,n,m,a))
    			r = mid -1;
    		else // 否则下界增大
    			l = mid + 1;
    	}
    	// 因为最后一次check满足的时候,r = mid -1
        // 而mid一直在变化,所以只能倒推出mid = r + 1;
    	return r+1; //答案为r+1,因为r+1是最后一个满足check的mid的值
    }
    
  • 进行二分求解

  • 为什么题目满足二分答案?

  • 具有单调性,d=6可分成m份,则d+1、d+2、…都可以划分成m份,

  • 反过来,如果d=5不可以分成m份,所以d-1、d-2、…也都不可以划分m份

  • 可以拆分成两部分

  • 画成01曲线,best的位置就是0突变成1的地方

  • 通过二分可以最终找出best

  • 检查是否可以分成m份

  • // 若能分成不超过m份,返回true,否则返回false
    bool check(long long d,int n,int m,vector<int> &a){
    	long long sum = 0;// 用于记录当前那一份的数字之和
    	int cnt = 1;// 在每一份数字之和不超过d的情况下,至少要分成的份数
    	
    	for(int i=0;i<n;i++){
    		if(a[i]>d) // 如果单个元素大于了d,直接返回false
    			return false;
    		sum += a[i];
    		if(sum>d){ // 将a[i]加入当前这一份中
    			sum = a[i]; // 将a[i]单独拿出来作为新的一组的开头
    			cnt = cnt +1; // 组数加1
    		}
    	}
    	return cnt<=m;
    }
    
  • 小技巧,看到”最大的最小“这样的描述或者类似的描述,转换成二分查找。

大转盘

  • 简要题意,给定整数n(1<=n<=16),有一个切成了2的n次方的圆盘,每块有一个数组(0或者1)。从第i个格子出发,顺时针走过n个格子,依次读出n个数字形成一个二进制数m
  • 构造一个圆盘,满足这些m形成一个0到2的n次方-1的排列
  • 下图中是n等于4的情况
  • 比如0010,然后转盘转动变成010,最后一位可以是0或者1,
  • 发现所有的点分别是000,001,010,011,100,101,110,111
  • 当001,添加一个0,则走到010这个点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cm1uDCPF-1605714344228)(C:\Users\liusiping\AppData\Roaming\Typora\typora-user-images\image-20201001120041033.png)]

  • 发现每个点的入度和出度都等于2
  • 结论:每个点的入度等于出度,就存在欧拉回路
  • 每走一条边,添加一个数字,走完所有的点,就是一条欧拉回路
  • 离散数学

解法1

  • 蛮力算法
  • 枚举0位置开始的数字,然后进行搜索(dfs)
  • 下一个位置的数字只有两种选择,在另一头加上0或者在另一头加上1
  • 若发现有重复的就不再深入往下搜索,怎么发现重复,用set记录

解法2

  • 我们将2的n次方二进制数看出一条边,一条边可能和一些边有关系,这个关系我们看成一个节点

  • 而这个关系就是一个n-1位的二进制数,这个“关系”恰好有两条边连入,有两条边连出,(对应着一头删掉,另一头加上)

  • 怎么理解,只看边,不看点,连接边的两个点刚好是变化的左右端点(0或1)

  • 而对这个建成的图每条边遍历一遍,就是答案

  • 每个点恰好有两条入边,两条出边,此时就存在欧拉回路

  • void dfs(int u){
    	for(int i=0;i<2;i++){
    		if(!vis(i)(u)){
                // 将u左移一位,然后将最低位置置为1,再将最高位去掉
    			int v = ((u<<1)|i) & allOne;
    			vis(i)(u) = 1;
                // 递归v,加入数字到ans中
    			dfs(v);
                // 先递归,再加入,所以最后得到的排列是一个逆序结果
                // 为什么先递归,是为了标记好找到每一条边的顺序
    			ans.push_back('0'+i);
    		}
    	}
    }
    
  • int allOne;// 全1,防止乘2越界
    vector<bool> vis[2];// vector的长度是欧拉图中点的数量,而每个点往下一个点移动都有0,1两种选择
    string ans;
    
    int twoPow(int x){
    	return 1<<x;
    }
    
    String getAnswer(int n){
    	allOne = twoPow(n-1) -1;
    	ans = "";
    	for(int i=0;i<2;i++)
    		vis[i].resize(twoPow(n-1),0);
    	
    	dfs(0);// 从0节点开始,搜出一条欧拉回路
    	return ans;// 因为是圆盘,无论顺序还是逆序都是对的
    }
    
  • 怎么构建一条边

    • ((u<<1)|i) &a
    • u<<1,相当于去掉首位
    • 再|i, 此处的i可能为0和1,表明了出边为两条,
    • &a,是对节点大小的限制,当节点是111这个量级时,保证不会因为<<1的操作而越界。

解法3

  • 提交答案

  • if(n==1)
    	cout << "1";
    if(n==2)
    	cout << "2";
    ....
    
    因为总共n是小于等于16
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值