一、排序算法
1、冒泡排序
重复走访过要排序的数列,一次比较相邻的两个元素,如果他们顺序错误就交换,直到没有再需要交换。两个for循环,第一个循环0len-1,第二个循环0len-1-i
void bubble_sort(int s[], int n){
int flag = 0;
for(int i = 0; i <= n - 1; i++){
flag = 0;
for (int j = 0; j <= n - i - 1; j++){
if(s[j] > s[j + 1]){
swap(s[j], s[j+1])
flag = 1;
}
}
if(flag == 0)
break;
}
}
2、选择排序
首先在未排序的序列中找出最大(小)元素,存放在排序序列的起始位置,然后再从剩余未排序的元素中继续寻找最大(小)元素,放到排序序列的末尾,直到所有元素均排序完毕。
void selection_sort(int s[], int n){
int min_index = 0;
for(int i = 0; i < n - 1; i++){
min_index = i;
for(int j = i + 1; j < n; j++){
if (s[min_index] > s[j]){
min_index = j;
}
}
if(min_index != i){
swap(s[i], s[min_index]);
}
}
}
3、插入排序
通过构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
void insert_sort(s[], n){
int tmp = 0;
for(int i = 0; i < n - 1; i++){
if (s[i] > s[i + 1]){
tmp = s[i + 1];
for(int j = i + 1; j >= 0 && tmp < s[j - 1]; j--){
s[j] = s[j - 1];
}
s[j] = tmp;
}
}
}
4、希尔排序
插入排序的升级版
- 根据某一增量将序列分为若干个子序列,并对子序列进行插入排序
- 然后逐渐将增量缩小,并重复上述过程,直到增量为1,此时数据基本有序,最后进行插入排序
void shell_sort(int s[], int len) {
int gap, i, j;
int temp;
while (gap < len / 3)
gap = gap * 3 + 1;
for (; gap > 0; gap /= 3)
for (i = gap; i < len; i++) {
temp = s[i];
for (j = i - gap; j >= 0 && s[j] > temp; j -= gap)
s[j + gap] = s[j];
s[j + gap] = temp;
}
}
5、快速排序
基本思想:分治思想 先从数列中取出一个数作为key值,将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边,对左右两个小数列重复第二步,直至区间只有1个数。
基本思想2:
- 在待排序的元素任取一个元素作为基准(通常选第一个元素,但最恰当的选择方法是从待排序元素中随机选取一个作为基准),称为基准元素(pivot);
- 将待排序的元素进行分区,比基准元素大的元素放在它的右边,比其小的放在它的左边;
- 对左右两个分区重复以上步骤直到所有元素都是有序的。
public class QuickSort {
public static void quickSort(int arr[],int _left,int _right){
int left = _left;
int right = _right;
int temp = 0;
if(left <= right){ //待排序的元素至少有两个的情况
temp = arr[left]; //待排序的第一个元素作为基准元素
while(left != right){ //从左右两边交替扫描,直到left = right
while(right > left && arr[right] >= temp)
right --; //从右往左扫描,找到第一个比基准元素小的元素
arr[left] = arr[right]; //找到这种元素arr[right]后与arr[left]交换
while(left < right && arr[left] <= temp)
left ++; //从左往右扫描,找到第一个比基准元素大的元素
arr[right] = arr[left]; //找到这种元素arr[left]后,与arr[right]交换
}
arr[right] = temp; //基准元素归位
quickSort(arr,_left,left-1); //对基准元素左边的元素进行递归排序
quickSort(arr, right+1,_right); //对基准元素右边的进行递归排序
}
}
public static void main(String[] args) {
int array[] = {10,5,3,1,7,2,8};
System.out.println("排序之前:");
for(int element : array){
System.out.print(element+" ");
}
quickSort(array,0,array.length-1);
System.out.println("\n排序之后:");
for(int element : array){
System.out.print(element+" ");
}
}
}
6、归并排序
采用分治法,对于包含m个元素的待排序序列,将其看成m个长度为1的子序列。然后两两进行归并,得到m/2个长度为2或者1的有序子序列;然后再两两归并,直到得到1个长度为m的有序序列
[外链图片转存中…(img-6smCsUmS-1648030223538)]
[外链图片转存中…(img-Aqa9IK3d-1648030223540)]
[外链图片转存中…(img-5YpvRYmZ-1648030223542)]
public class MergeSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}
7、堆排序
堆排序是一种选择排序,利用堆这种数据结构来完成。其算法思想是将待排序的数据构造成一个最大堆(升序)或最小堆(降序),然后将堆顶元素与待排序数组的最后一个元素交换位置,此时末尾元素就是最大或最小的值。然后将剩余n-1个元素重新构造成最大堆或最小堆。
基本思想描述:
- 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
- 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
二、辗转相除法
1、求最大公约数,最小公倍数
#include<stdio.h>
int divisor(int m, int n){
int r;
while(n != 0){
r = m % n;
m = n;
n = r;
}
return m;
}
int multiple(int m, int n){
int result = (m * n)/divisor(m, n);
return result;
}
int main(){
int m, n;
scanf("%d %d", &m, &n);
int div = divisor(m, n);
int mul = multiple(m, n);
printf("%d, %d的最大公约数:%d, 最小公倍数:%d", m, n, div, mul);
return 0;
}
三、素数判断
#include<stdio.h>
#include<math.h>
int isPrime(int n){
int flag = 1;
for(int i = 2; i <= sqrt(n); i++){
if(n % i == 0){
flag = 0;
break;
}
}
}
int main(){
int n;
scanf("%d", &n);
int result = isPrime(n);
if(result){
printf("%d is a prime number", n);
}
else{
printf("%d is not a prime number", n);
}
return 0;
}
四、判断闰年
#include<stdio.h>
#include<math.h>
int isLeapYear(int y){
int flag = 0;
if ((y % 400 == 0) || (y % 4 == 0 && y % 100 != 0))
flag = 1;
return flag;
}
int main(){
int y;
scanf("%d", &y);
int result = isLeapYear(y);
if(result){
printf("%d is leap year", y);
}
else{
printf("%d is not leap year", y);
}
return 0;
}
五、查找算法
1、顺序查找
顺序查找适合于存储结构为顺序结构或者链接结构的线性表,顺序查找也称为线性查找,属于无序查找,从线性表的一端开始查找,查找到最后。
顺序查找分析:
-
缺点是速度慢,平均查找长度为 (n + 1) / 2,时间复杂度为 O(n) .
-
优点是即适用于顺序表,也适用于单链表,同时对表中元素排列次序无要求,给插入和删除元素带来了方便。
2、二分查找
二分查找又称折半查找。
作为二分查找对象的表必须是顺序存储的有序表,通常假定有序表是按关键字从小到大有序。
查找过程是首先取整个有序表 A[0] ~ A[n - 1] 的中点元素 A[mid] (mid = (0 + n -1) / 2) 的关键字同给定值 K 比较,相等则成功,若 K 较小,则对 剩余的左半部分进行同样
操作,若 K 较大,则对其剩余的右半部分进行同样的操作。
int Binsch(struct ElemType A[], int low, int high, KeyType K)//递归法
{//在 A[low] ~ A[hight]区间进行查找,low、hight初值分别为 0 和 n-1
if (low <= hight)
{
int mid = (low + high) / 2; //求中点元素下标
if (K == A[mid].key)
return mid;
else if (K < A[mid].key)
return Binsch(A, low, mid - 1, K);
else
return Binsch(A, mid + 1, high, K);
}
else
return -1; //查找失败
}
int Binsch1(struct ElemType A[], int low, int high, KeyType K)//非递归法
{//在 A[low] ~ A[hight]区间进行查找,low、hight初值分别为 0 和 n-1
while (low <= high)
{
int mid = (low + high) / 2; //求中点元素下标
if (K == A[mid].key)
return mid;
else if (K < A[mid].key)
high = mid - 1;
else
low = mid + 1;
}
else
return -1; //查找失败
}
优缺点:
二分查找的优点是比较次数少,速度快,但在查找之前要为建立有序表付出代价,同时对有序表的插入和删除也较为费力。
二分查找适用于数据相对稳定的情况,而且只适用于顺序存储的有序表,不适用链接存储的有序表。
3、插值查找
插值查找实际就是二分查找的改版,原理如下:
[外链图片转存中…(img-6MNhsIqy-1648030223544)]
举例如下:
[外链图片转存中…(img-fnrp7Fu6-1648030223549)]
public static int insertValueSearch(int[] arr,int left,int right,int value){
//递归结束
//没有找到
//重点:这里value < arr[0] || value > arr[arr.length - 1]不能省
//原因:由于需要查找的元素涉及到了求中值的算法运算,根据算法公式可知随着value的增大,mid也随之增大
//如果如果value过于大时,mid可能越界
if (left > right || value < arr[0] || value > arr[arr.length - 1]){
return -1;
}
//改变过后求中间下标的算法
int mid = left +(right - left) * (value - arr[left]) / (arr[right] - arr[left]);
int midValue = arr[mid];
if (value > midValue){//右递归
return insertValueSearch(arr,mid + 1,right,value);
}else if (value > midValue){//左递归
return insertValueSearch(arr,left,mid - 1,value);
}else {//找到了
return mid;
}
}
六、贪心算法
**算法思想:**贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
基本思路:
- 建立数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的解局部最优解合成原来解问题的一个解。
存在的问题:
- 不能保证求得的最后解是最佳的;
- 不能用来求最大或最小解问题;
- 只能求满足某些约束条件的可行解的范围。
算法实现过程:
- 从问题的某一初始解出发;
- while 能朝给定总目标前进一步 do;
- 求出可行解的一个解元素;
- 由所有解元素组合成问题的一个可行解。
贪心选择性质:
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
最优子结构性质:
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
贪心算法一般流程:
TypeSolution Greedy(C) //C是问题的输入集合即候选集合
{
S={ }; //初始解集合为空集
while (not solution(S)) //集合S没有构成问题的一个解
{
x=select(C); //在候选集合C中做贪心选择
if feasible(S, x) //判断集合S中加入x后的解是否可行
S=S+{x};
C=C-{x};
}
return S;
}
七、动态规划算法
动态规划概念:
动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
# from Wiki: dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.
# 一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。
核心思想:
动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算。
例子如下:
'''
A :"1+1+1+1+1+1+1+1 =?"
A :"上面等式的值是多少"
B :计算 "8"
A : 在上面等式的左边写上 "1+" 呢?
A : "此时等式的值为多少"
B : 很快得出答案 "9"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"
'''
动态规划典型特征:
- 最优子结构
- 状态转移方程
- 边界
- 重叠子问题
动态规划解题思路:
LeetCode原题:
# 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。
总体思路:
- 穷举分析
- 确定边界
- 找出规律,确定最优子结构
- 写出状态转移方程
- 穷举分析
当台阶数是1的时候,有一种跳法,f(1) =1
当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;
当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然 后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3
当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然 后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5
当台阶是5级时…
[外链图片转存中…(img-5DrbHmQa-1648030223551)]
- 确定边界
通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2, 当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。
- 找规律
n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子 结构?有这么一个解释:
# 一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质
- 写出状态转移方程
通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:
[外链图片转存中…(img-wT3TawO6-1648030223553)]
- 代码实现
dp[][][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}
青蛙跳阶的代码实现
public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = ;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}
八、DFS
前言:
深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念。
算法实现一般采用数据结构中的队列
搜索过程举例:
[外链图片转存中…(img-GnkjhuC3-1648030223554)]
- V0->V1->V4,此时到底尽头,仍然到不了V6,于是原路返回到V1去搜索其他路径;
- 返回到V1后既搜索V2,于是搜索路径是V0->V1->V2->V6,,找到目标节点,返回有解。
处理过程:
[外链图片转存中…(img-xvdPfnC4-1648030223559)]
[外链图片转存中…(img-MPhBSJNh-1648030223560)]
[外链图片转存中…(img-SJ4N3Y2h-1648030223562)]
[外链图片转存中…(img-whKdgrBz-1648030223563)]
[外链图片转存中…(img-hjvX6rjl-1648030223565)]
[外链图片转存中…(img-bhl9iVsh-1648030223569)]
[外链图片转存中…(img-v7jAB9Rm-1648030223570)]
[外链图片转存中…(img-kAC1kQi9-1648030223574)]
[外链图片转存中…(img-q7i1DD7q-1648030223575)]
[外链图片转存中…(img-uMwSX6cp-1648030223577)]
核心代码:
/**
* DFS核心伪代码
* 前置条件是visit数组全部设置成false
* @param n 当前开始搜索的节点
* @param d 当前到达的深度
* @return 是否有解
*/
bool DFS(Node n, int d){
if (isEnd(n, d)){//一旦搜索深度到达一个结束状态,就返回true
return true;
}
for (Node nextNode in n){//遍历n相邻的节点nextNode
if (!visit[nextNode]){//
visit[nextNode] = true;//在下一步搜索中,nextNode不能再次出现
if (DFS(nextNode, d+1)){//如果搜索出有解
//做些其他事情,例如记录结果深度等
return true;
}
//重新设置成false,因为它有可能出现在下一次搜索的别的路径中
visit[nextNode] = false;
}
}
return false;//本次搜索无解
}
九、BFS
前言:
广度优先搜索(也称宽度优先搜索,缩写BFS,以下采用广度来描述)是连通图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,故得名。
一般可以用它做什么呢?一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。
算法导论里边会给出不少严格的证明,我想尽量写得通俗一点,因此采用一些直观的讲法来伪装成证明,关键的point能够帮你get到就好。
一般使用数据结构栈来实现
算法基本思路:
常常我们有这样一个问题,从一个起点开始要到一个终点,我们要找寻一条最短的路径,从图2-1举例,如果我们要求V0到V6的一条最短路(假设走一个节点按一步来算)【注意:此处你可以选择不看这段文字直接看图3-1】,我们明显看出这条路径就是V0->V2->V6,而不是V0->V3->V5->V6。先想想你自己刚刚是怎么找到这条路径的:首先看跟V0直接连接的节点V1、V2、V3,发现没有V6,进而再看刚刚V1、V2、V3的直接连接节点分别是:{V0、V4}、{V0、V1、V6}、{V0、V1、V5}(这里画删除线的意思是那些顶点在我们刚刚的搜索过程中已经找过了,我们不需要重新回头再看他们了)。这时候我们从V2的连通节点集中找到了V6,那说明我们找到了这条V0到V6的最短路径:V0->V2->V6,虽然你再进一步搜索V5的连接节点集合后会找到另一条路径V0->V3->V5->V6,但显然他不是最短路径。
你会看到这里有点像辐射形状的搜索方式,从一个节点,向其旁边节点传递病毒,就这样一层一层的传递辐射下去,知道目标节点被辐射中了,此时就已经找到了从起点到终点的路径。
我们采用示例图来说明这个过程,在搜索的过程中,初始所有节点是白色(代表了所有点都还没开始搜索),把起点V0标志成灰色(表示即将辐射V0),下一步搜索的时候,我们把所有的灰色节点访问一次,然后将其变成黑色(表示已经被辐射过了),进而再将他们所能到达的节点标志成灰色(因为那些节点是下一步搜索的目标点了),但是这里有个判断,就像刚刚的例子,当访问到V1节点的时候,它的下一个节点应该是V0和V4,但是V0已经在前面被染成黑色了,所以不会将它染灰色。这样持续下去,直到目标节点V6被染灰色,说明了下一步就到终点了,没必要再搜索(染色)其他节点了,此时可以结束搜索了,整个搜索就结束了。然后根据搜索过程,反过来把最短路径找出来,图3-1中把最终路径上的节点标志成绿色。
[外链图片转存中…(img-dQ34MCi8-1648030223578)]
[外链图片转存中…(img-psqWhqAW-1648030223579)]
[外链图片转存中…(img-QuXL1bOk-1648030223584)]
[外链图片转存中…(img-APcWhCgV-1648030223587)]
[外链图片转存中…(img-gL28K32O-1648030223588)]
算法流程图:
[外链图片转存中…(img-pmXKCmCn-1648030223589)]
核心代码:
/**
* 广度优先搜索
* @param Vs 起点
* @param Vd 终点
*/
bool BFS(Node& Vs, Node& Vd){
queue<Node> Q;
Node Vn, Vw;
int i;
//初始状态将起点放进队列Q
Q.push(Vs);
hash(Vw) = true;//设置节点已经访问过了!
while (!Q.empty()){//队列不为空,继续搜索!
//取出队列的头Vn
Vn = Q.front();
//从队列中移除
Q.pop();
while(Vw = Vn通过某规则能够到达的节点){
if (Vw == Vd){//找到终点了!
//把路径记录,这里没给出解法
return true;//返回
}
if (isValid(Vw) && !visit[Vw]){
//Vw是一个合法的节点并且为白色节点
Q.push(Vw);//加入队列Q
hash(Vw) = true;//设置节点颜色
}
}
}
return false;//无解
}
十、单链表
参考杨路明C语言教材第四版链表那一节(创建,插入,删除,遍历,搜索)
param Vs 起点
-
@param Vd 终点
*/
bool BFS(Node& Vs, Node& Vd){
queue Q;
Node Vn, Vw;
int i;//初始状态将起点放进队列Q
Q.push(Vs);
hash(Vw) = true;//设置节点已经访问过了!while (!Q.empty()){//队列不为空,继续搜索!
//取出队列的头Vn
Vn = Q.front();//从队列中移除 Q.pop(); while(Vw = Vn通过某规则能够到达的节点){ if (Vw == Vd){//找到终点了! //把路径记录,这里没给出解法 return true;//返回 } if (isValid(Vw) && !visit[Vw]){ //Vw是一个合法的节点并且为白色节点 Q.push(Vw);//加入队列Q hash(Vw) = true;//设置节点颜色 } }
}
return false;//无解
}
[深度优先算法参考博客](https://blog.csdn.net/raphealguo/article/details/7523411)
[视频教程](https://www.bilibili.com/video/BV1254y1976m?spm_id_from=333.337.search-card.all.click)
## 十、单链表
参考杨路明C语言教材第四版链表那一节(创建,插入,删除,遍历,搜索)