先声明一下所有代码都是直接手打的 ,没有复制粘贴,有些比较难的算法我就是到网上找视频,跟着他/她敲一遍理解其中的意义所在。
一、归并排序
之前归并排序没有怎么搞懂,这一周又仔仔细细的看了一遍,总结出来的知识。
归并排序实质是用了完全二叉树来设计的,和堆排序有异曲同工之处,接下来看一张图
这是我在知乎上下的一张归并排序的过程图, 可以观察箭头上有分割,也有归并的字样,我当时就很疑惑,为什么归并法会有分割呢?说到这个问题就会涉及到归并的基本思想了。
基本思想
1.分割:将长度为n的序列分割成两个子序列,即n/2;
2.排序:对两个子序列进行排序。
3.归并:将排序好的子序列进行合并。
说完基本思想我们再来看看代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void merge(int arr[], int tempArr[], int left, int mid, int right);//归并并排序
void merge_sort(int arr[], int n);//归并排序入口
void msort(int arr[], int tempArr[], int left, int right);//分割
main()
{
int a[6] = { 14,12,15,13,11,16 };
for (int i = 0; i < 6; i++)
{
printf("%d ", a[i]);
}
printf("\n");
merge_sort(a, 6);
for (int i = 0; i < 6; i++)
{
printf("%d ", a[i]);
}
}
void merge_sort(int arr[], int n)
{
int * temp = (int *)malloc(sizeof(int) * n);//动态分配一个
//临时数组,为了起到一个中间变量来存放合并并排序的数组
msort(arr, temp, 0, n - 1);//第一次是0和最后一个
free(temp);
}
void msort(int arr[], int tempArr[], int left, int right)
{
int mid = (right+left) / 2;//将原本串分割成两个串
if (left < right)//直到做到仅剩一个节点为止
{
//左分割
msort(arr, tempArr, left, mid);
//右分割
msort(arr, tempArr, mid+1, right);
//看到这里如果学过树的遍历的话,就会想到,这是不是就是树的后序遍历
merge(arr, tempArr, left, mid, right);
}
}
void merge(int arr[], int tempArr[], int left, int mid, int right)
{
int lpos,rpos,i=left;//左右字串的指针,准备进行合并
lpos = left;
rpos = mid + 1;
while (lpos <=mid && rpos <=right)//左右字串都还有
{
if (arr[lpos] < arr[rpos]) { tempArr[i++] = arr[lpos++]; }
else { tempArr[i++] = arr[rpos++]; }
}
while (lpos <= mid)//将剩余的数放入
{
tempArr[i++] = arr[lpos++];
}
while (rpos <= right)
{
tempArr[i++] = arr[rpos++];
}
while(left<=right)
{
arr[left] = tempArr[left];
left++;
}
}
为了方便理解,我将数组改成和上图一样的数了, 我有三个子函数,分别对应着归并排序法基本思想的三点,msort是对应着“割”,merge是对应着“归并和排序”,merge_sort对应的是整个的初始化,也是入口,在“割”的函数中,如果学过树的话这里也可以运用到树的后序遍历来方便理解,也就是说,你给我一串序列,我先将它不断分割,直到分割到只剩下了一个节点,不能分割为止,我再将它按顺序一个个合并起来,最终成为一个有序的序列。我当时想不清楚为什么需要将它一个个分割成只有一个才进行排序,这样吧我举个例子,首先扯到我们高考成绩,知道两人的成绩高低很简单,比如甲比乙分数高,丙比丁分数高,那我们很容易就得到甲乙丙丁的成绩高低了,直知道这四位同学的成绩高低我们又可以再找四位同学也像这个方案去比,这样我们就得到了八个人的成绩高低,继续这样下去就可以得到全省的成绩高低,你的成绩就会出来了,喔,吉首大学,那太棒了。
学排序不讲空间复杂度和时间复杂度,就是白学。
时间复杂度:O(nlogn)
空间复杂度:O(n+logn)
总结:比较占内存,但效率极高稳定。
二、快速排序
如果你还不会快速排序法的话,一定要去学,快速两个子就可以看出这个算法的牛逼之处了。这个算法被列为20世纪十大算法之一!!还有什么理由不学呢?
还是老样子聊聊基本思想:
基本思想:将当前序列第一个作为关键字,通过一趟排序将当前序列分为两段,左段比关键字小,右段比关键字要大,然后通过递归再将左段和右段通过刚刚的方式又分,最终成为一个有序的数列。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void kuaisu(int a[], int L, int R)
{
int right, left,piovet;
if (L >= R) return;
right = R;
left = L;
piovet = a[L];//默认第一个为目标量,备份一份,防止被覆盖
while (right > left)
{
while (left < right && a[right] >= piovet)
{
right--;
}//出来要么是打通关了,要么就是找到一个比piovet小的数了
if (left < right) { a[left] = a[right]; left++; }
while (left < right && a[left] <= piovet)
{
left++;
}//出来要么是打通关了,要么就是找到一个比piovet大的数了
if (left < right) { a[right] = a[left]; right--; }
if (left >= right) a[left] = piovet;//当left大于right时就表明了,左半区和右半区找完了
//那么剩下的那个位置就是目标数的,那么就上皇位a[left]
if(L<left)
kuaisu(a, L, left - 1);//比目标量小的放在左半区
//左半区再进行递归又进行排
if(left<R)
kuaisu(a, left + 1, R);//比目标量大的放在右半区
}
}
int main()
{
int a[10],i;
for (i = 0; i <10 ; i++)
{
scanf("%d", a + i);
}
kuaisu(a, 0, 9);
for (i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
}
void xuanzefa()
{
int i, j, p, t, a[4];
printf("\n input 10 numbers:\n");
for (i = 0; i < 10; i++)
scanf("%d", &a[i]);
for (i = 0; i < 10; i++) //第i轮排序
{
p = i;
for (j = i + 1; j < 10; j++) //找最小元素
if (a[j] < a[p]) p = j;
if (i != p) { t = a[i]; a[i] = a[p]; a[p] = t; } //交换
printf("%8d", a[i]);
}
}
kuaisu()这个函数做的就是先选取一个关键字,想尽办法将比关键字小的放在左边,将比关键字大的放在右边,接下来我们来说本世纪十大算法的时间复杂度和空间复杂度a
时间复杂度:最好O(nlogn)最差O()
空间复杂度:最好O(logn),最差O(n)
三、bfs(Breadth First Search)层层递进
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。------百度百科
基本思想和跟dfs的联系和区别
首先它和dfs一样,是一种图的搜索方式,可以发现百度百科中说它思想用在最短路径和最小生成树很多,为什么会应用在这些求最小,最短的问题上呢?,这就和bfs的搜索方式拉上关系了,我们上一周学到了dfs,dfs它是不撞南墙不回头,就是一条路径深入搜索直到没有路走为止,而bfs不一样,它属于一种层序遍历,也就是说bfs它先找跟根节点距离为1的所有节点,找完之后再去找距离为2的所有节点,再找距离为3的所有节点......,从小到大依次遍历,所以广度优先遍历一般是求解最短路径问题。
bfs的实现
bfs实现一般是用队列来实现,将根节点入队头,每一次将队头取出,将这个元素所有距离为一的元素,全部入队。
代码实现
我们这里还是走迷宫找出口求最短路径问题的题目。代码逻辑比较简单,其中有很多注释
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include <stdbool.h>
struct note
{
int x;//x坐标
int y;//y坐标
int note;//从源节点走到这个节点步数
};
struct note que[2501];//队列,这里也可以用循环队列来节省空间
int head, tail;//head队头,tail队尾
int map[51][51]={0};//地图,用来记录障碍的位置
bool trace[51][51] = { 0 };//判断是否走过
int dx[4] = { 1,0,-1,0 };
int dy[4] = { 0,1,0,-1 };//代表四个方向,分别是右下左上
main()
{
int c, k,i,j,tx,ty,startx,starty,endx,endy;
int flag = 0;//判断是否到出口
scanf("%d %d", &startx, &starty);//输入入口
scanf("%d %d", &endx, &endy);//输入出口
head = tail = 1;
que[tail].x = startx;
que[tail].y = starty;
que[tail].note = 0;//到自己的步数为0
//将源节点入队
tail++;//源节点入队后,队尾指针应向后移动一个
trace[startx][starty] = true;//将源节点即迷宫入口标记为走过
//初始化完成
scanf("%d %d", &c, &k);//输入迷宫大小
for (int i = 1; i <= c; i++)
{
for (int j = 1; j <= k; j++)
scanf("%d", &map[i][j]);//1代表着障碍,0代表着可以走
}
while (head < tail)//如果它不是空队,就一直做
{
for (i = 0; i < 4; i++)
{
tx = que[head].x + dx[i];
ty = que[head].y + dy[i];
if (tx<1 || tx>k || ty<1 || ty>c)//判断是否越界
continue;
if (map[tx][ty] == 0 && trace[tx][ty] == 0)
{//不是障碍,并且没有走过
trace[tx][ty] = 1;//将它标记为走过了
que[tail].x = tx;
que[tail].y = ty;//将该点入队
que[tail].note = que[head].note + 1;//到源节点的步数
tail++;
//入队完成
}
if (tx == endx && ty == endy)//如果到达出口了就不做了
{
flag = 1;
break;
}
}
if (flag == 1)
{
break;
}
head++;//相当于出列
}
printf("%d", que[tail - 1].note);
}
队列很重要,对这一道题目!!图的遍历就讲完了呼累啊。
四、滑动窗口
ACM社团考了一个数组问题:螺旋数组,当我写完这个的时候想要将数组的那一块思想给总结一下,我就领出来了一个困惑我比较久的数组的思想,那就是滑动窗口,leetcode上有一道题目叫求最小长度字串,这道题目就是经典的滑动窗口。
题目描述:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
为了体现出优势,我们这边在leetcode上拷贝一个c++的暴力解法
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
这个用了两个for循环,不断地去寻找合适地子序列,时间复杂度为O()
我们再来看看滑动窗口
先说说基本思想:
首先要解决三个问题:
1.窗口里面是什么?
2.如何对窗口地起始位置和结束位置进行移动?
1.窗口里面时满足当前和>=s的长度最小的连续子数组
2.当出现>=s了就要窗口起始位置将向前移动(寻找下一个满足>=s的子序列)
3.窗口结束位置就是遍历数组的指针就是for循环里面一直变的j
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
看代码(暑假发布在leetcode上的代码)跟着代码做一遍就会发现它的思想所在了
#define _CRT_SECURE_NO_WARNINGS 1
nt minSubArrayLen(int target, int* nums, int numsSize){
//初始化最小长度为INT_MAX
int mlenth = INT_MAX;
int sum = 0;
int left = 0, right = 0;
//右边界向右扩展
for(; right < numsSize; ++right) {//即结束位置,这就是滑动窗口的精妙,它是结束位置一直在往后移动,而起始位置要通过判定才会移动
sum += nums[right];
//当sum的值大于等于target时,记录长度,并且收缩左边界,即又到回到<s的情况
//就要通过滑动终止位置(j)来继续回到>=s的情况,然后依次类推
while(sum >= target) {//注意这里一定要while,因为你要想,若j移动到一个非常大的数,那么我的起始位置就不一定只移动一个,而是很多个
int lenth = right - left + 1;
mlenth = mlenth < lenth ? mlenth : lenth;
sum -= nums[left++];
}
}
//若mlenth存在的话就返回最小长度长度,若不存在就返回0,表示没有该数列
return mlenth == INT_MAX ? 0 : mlenth;
}
注意看我的注释,写的很详细哦。
五、大数(+,-,*,/)
这是ACM社的竞赛作业,妈耶,我看到这个提交量,和通过率我就觉得不简单,绝对有坑,我也踩进去了哈哈哈哈
顾名思义,大数就是一个很大的数,是我们口中讲的天文数字一样,很有可能会超过32位和64位,所以我们无法对它进行1直接的加减乘除,那我们应该怎么办呢?用字符串,数组,链表,去模拟加减乘除的计算过程,注意是模拟
先来讲大数加法:
大数加法是最简单的,我们要在脑海里面模拟一下,我们平常做加法的时候是怎么做的,从右往左依次相加,遇到超过10的就进位是不是 。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
main()
{
char a[100000], b[100000];
int z[10000000], i;
int lentha, lenthb,n1,n2,sum;
int ys=0;//余数
scanf("%s%s", a, b);
lentha = strlen(a) - 1;
lenthb = strlen(b) - 1;
while (lentha >= 0 || lenthb >= 0)
{
n1 = a[lentha--] - '0';//将字符变成数字;
n2 = b[lenthb--] - '0';
sum = n1 + n2 + ys;
ys = sum / 10;
z[i++] = sum % 10;
}
if (ys > 0)//最后一个还要进位
{
z[i] = ys;
}
}
比较简单就直接看代码吧
接下来是大数的减法了
还是一样,现在脑海里面模拟一下我们小学是怎么算减法的。
是不是发现了加法和减法的区别了,加法是进位,而减法的话就是借位,如果是正好一个大一个小,减起来比较方便,但是一个小数减去一个大数的话那就是还要加一个负号,并且处理起来比较难,所以我们就想,可不可以都换成是大数减小数呢?当然可以,因为1-3=-(3-1),只要在结果上加一个负号就行了,代码实现:
代码以后补,出了一点小问题,不知道怎么解决,先放着,下周来