一:算法基础
算法是一个用于解决特定问题的有限指令序列(计算机可以执行的操作)。通俗的理解就是可以解决特定问题的方法。
算法的三种表示形式:
- 伪代码
- 自然语言
- 流程图
算法的五个特性:
- 有穷性: 一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。比如,写递归函数的时候千万当心。(算法是有穷的,但执行中的程序可以是无穷的,一直在运行)
- 确定性:算法中每一条指令必须有确切的含义,不存在二义性。
- 可行性: 算法描述的操作都可以通过已经实现的基本运算执行有限次数来实现。(不能实现的算法要他何用?)
- 输入: 有零个或多个输入,以刻画运算对象的初始情况。
- 输出: 有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的。
好算法的设计目标:
(1)正确性: 算法应能够正确地解决求解问题。这是最重要也是最基本的标准。“正确”又分为如下四个层次:
- 程序不含语法错误。
- 对于几组测试数据,能得出满足要求的结果。
- 对于精心设计的、典型而苛刻的几组输入数据能得出满足要求的结果。
- 对于一切合法的输入数据,都能得到满足要求的结果。
(2)可读性: 算法应容易供人阅读和交流。可读性好的算法有助于对算法的理解和修改。
(3)健壮性: 算法应具有容错处理。当输入非法或错误数据时,算法应能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。
(4)通用性: 算法应具有一般性 ,即算法的处理结果对于一般的数据集合都成立。
(5)高效率与低存储量: 效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空间。一般地,这两者与问题的规模有关。
(1)算法评价标准1:时间复杂度(效率的度量)
算法执行时间需通过依据该算法编写的程序在计算机上运行所消耗的时间来度量。方法通常有两种:
- 方式1 事后统计:计算机内部进行执行时间和实际占用空间的统计。问题:必须先运行依据算法编写的程序;依赖软硬件性能(计算机硬件性能、不同的编程语言),容易掩盖算法本身的优劣;没有实际价值。
- 方式2 事前分析:求出该算法的一个时间界限函数。
与此相关的因素有:
- 依据算法选用何种策略
- 问题的规模
- 数据的初始状态
- 程序设计的语言
- 编译程序所产生的机器代码的质量
- 机器执行指令的速度
这里化繁为简,抛开硬件和编程语言的影响外,可以认为一个特定算法时间度量T的大小,只依赖于问题的规模(通常用n表示),表示成是问题规模的函数f(n)。
时间复杂度的理解
时间复杂度不是执行完一段程序的总时间,而是描述为一个算法中基本操作的总次数。
算法中基本操作重复执行的次数是问题规模n的某个函数f(n),其时间量度记作 T(n)=O(f(n)),称作时间复杂度。这里的n称为问题的规模,如要处理的数组元素的个数为n,则基本操作所执行的次数是n的一个函数f(n)。
大O渐进表示法:大O符号是用于描述函数渐进性的数学符号。
#include <stdio.h>
void PlayGame(int n) // n为问题规模
{
int i = 1; // 执行1次
while (i <= n) // 执行 n + 1次
{
printf("数据已加载%.2lf%,即将开始\n", (i + 0.0) / n * 100); // 执行n次
i++; // 执行n次
}
printf("数据加载完毕,开始战斗吧!"); // 执行1次
}
int main()
{
playGame(200);
return 0;
}
此时,T(n) = 1 + (n + 1) + n + n + 1 = 3n + 3。
时间复杂度关系:
其中k > 3,后三项称为指数时间复杂度,前面的各项称为多项式时间复杂度。
常量阶O(1):
耗时与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。
常用算法或场景:一般情况下,一个没有循环(或有循环,但循环次数与问题规模n无关)的算法中,原操作执行次数与问题规模n无关,记作O(1),也称常数阶。
线性阶O(n):
代表数据量增大几倍,耗时也增大几倍。常用算法或场景:遍历、查找等。
void Sum2(int n){
for (int i = 1; i <= n; i++) {
printf("test"); //执行n次
}
}
平方阶O(n^2):
常用算法或场景:双层遍历、冒泡排序算法、插入排序算法等。
void Add(int x, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
x = x + 1;
}
}
}
立方阶O(n^3)
int sum = 0; // 执行1次
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
for (int k = 0; k < n; k++)
{
sum++; // 执行n*n*n次
}
}
}
return sum; // 执行1次
}
int main()
{
int sum = test1(5);
printf("sum = %d\n", sum); // 125 => 5^3
}
对数阶O(log2n)
当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。
常见场景:二分查找(每找一次排除一半的可能)。
void test1(int n)
{
int i = 1;
while (i <= n)
{
i *= 2;
}
}
线性对数阶O(nlogn):
典型代表:快速排序、归并排序、堆排序
void test3(int n)
{
int x;
for (int i = 0; i < n; i++)
{
x = 1;
while (x < n / 2)
{
x = 2 * x;
}
}
}
平方根阶O(sqrt(n)):
void fun(int n)
{
int i = 0, s = 0;
while (s < n)
{
// 基本操作
i++;
s = s + i;
}
}
(2)算法评价标准2:空间复杂度(存储量的度量)
空间复杂度的理解
算法的空间复杂度是指算法在运行过程中除了算法本身的指令、常数外,还需要的针对数据操作的辅助存储空间的大小。同样是问题的规模(或大小)n的函数。
空间复杂度(Space complexity) ,记作:S(n)=O(f(n))。
常见的空间复杂度主要有:O(1) 和 O(n)。
常见的空间复杂度关系
其中k > 3。
二:查找算法
(1)顺序查找
顺序查找又称线性查找,是一种基本的查找算法,其原理是:
- 从头开始遍历:从数据集的起始位置开始,逐个检查每个元素。
- 比较目标:对于每个遍历到的元素,将其与目标元素进行比较。
- 查找成功:如果当前元素等于目标元素,则查找成功,返回当前元素的索引。
- 查找失败:如果遍历完整个数据集仍未找到目标元素,则查找失败,返回一个特殊的标识来表示未找到。
#include <stdio.h>
/**
* @brief
* 顺序查找:就是设计一个函数,函数可以让你找到数组当中某一个元素---角标
* @return ** int
*/
// 第一个参数:数组 第二个:数组长度 第三:要查找的元素
int orderSeach(int array[], int length, int element)
{
for (int i = 0; i <= length - 1; i++)
{
if (array[i] == element)
{
return i;
}
}
return -1;
}
int main()
{
// 数组
int arr[] = {-78, 88, 123, 0, -36, 787, 4421};
// 数组的长度
int length = sizeof(arr) / sizeof(int);
int index = orderSeach(arr, length, 0);
printf("0这个元素的角标:%d\n", index);
return 0;
}
(2)二分查找
二分查找(Binary Search)是一种高效的搜索算法,通常用于有序数据集中查找目标元素。其原理是通过将数据集划分为两半并与目标进行比较,以确定目标在哪一半中,从而逐步缩小搜索范围,直到找到目标元素或确定不存在。基本原理如下:
- 选择中间元素: 在有序数据集中,选择数组的中间元素。
- 比较目标: 将中间元素与目标元素进行比较。
- 查找成功: 如果中间元素等于目标元素,则查找成功,返回中间元素的索引。
- 缩小搜索范围: 对于一个升序的数据集,如果中间元素大于目标元素,说明目标可能在左半部分;如果中间元素小于目标元素,说明目标可能在右半部分。根据比较结果,将搜索范围缩小到一半,继续查找。
- 重复步骤: 重复上述步骤,不断将搜索范围缩小,直到找到目标元素或搜索范围为空。
#include <stdio.h>
/**
* 应用的场景:数组务必是有序的,递增、递减
* 查找元素的索引的
*
*
*/
// 实现二分查找
// 返回之代表的是返回角标
// 第一个参数:查找元素角标的起始数组
// 第二个元素:数组元素的长度
// 第三个参数:查找的元素数据
int binarySearch(int arr[], int length, int element)
{
// 左侧角标
int left = 0;
int right = length - 1;
// left<=right,说明数组内部还有元素,接着继续!!!!
while (left <= right)
{
int middle = (left + right) / 2;
// 判断是否中间的这个即为查找的元素
if (arr[middle] == element)
{
return middle;
}
if (arr[middle] < element)
{
left = middle + 1;
}
if (arr[middle] > element)
{
right = middle - 1;
}
}
return -1;
}
int main()
{
int arr[] = {10, 17, 88, 129, 256, 278, 369};
// 需要数组的长度
int length = sizeof(arr) / sizeof(int);
int index = binarySearch(arr, length, 129);
printf("%d\n", index);
return 0;
}
三:排序算法
(1)十大常见的排序算法
排序算法很多,实现方式各不相同,时间复杂度、空间复杂度、稳定性也各不相同
(2)冒泡排序
冒泡排序原理如下:
- 比较相邻元素: 从数组的第一个元素开始,依次比较相邻的两个元素。
- 交换位置: 如果前面的元素比后面的元素大(或小,根据升序或降序排序的要求),则交换这两个元素的位置。
- 一趟遍历完成后,最大(或最小)元素已移至末尾: 经过一趟遍历,最大(或最小)的元素会被交换到数组的最后一个位置。
- 重复进行遍历和交换: 除了最后一个元素,对数组中的所有元素重复执行上述两步,每次遍历都会将当前未排序部分的最大(或最小)元素放置到合适的位置。
- 循环遍历: 重复执行步骤3和步骤4,直到整个数组都被排序。
#include <stdio.h>
/**
* 冒泡排序:将无序的数组,变为有序的数组【递增、递减】
*
* 如果说数组五个元素:比较四轮
* 如果说十个元素:比较九轮
*
*
*
*/
// 准备一个打印的函数-->打印数组里面的元素
void print(int arr[], int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
// 初始化的数组
int arr[] = {3, 1, 5, 4, 2,99,-4,120};
// 长度
int length = sizeof(arr) / sizeof(int);
// 外层循环语句代表比较轮数
for (int k = 1; k < length; k++)
{
for (int i = 0; i < length - k; i++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
}
// 输出打印
print(arr, length);
return 0;
}
(3)快速排序
快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止排序算法中速度最快的一种,快速排序的时间复杂度为O(nlog(n))。
快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。
排序思想:
从数列中挑出一个元素,称为"基准"(pivot),
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
#include <stdio.h>
/**
*基准值:数组的首个元素
low:从左往右找到第一个比基准值大的元素
height:从右向左找到第一个比基准值小的元素 low>height = 交换
如果low与height交叉 基准值与height进行交换
*
*
*/
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
// 第一个参数:数组
void quickSort(int arr[], int start, int end)
{
// 递归最终结束条件
if (start >= end)
{
return;
}
// 角标low
int low = start;
// 角标height
int height = end + 1;
while (1)
{
// 从左到右找到low第一个比基准值大的
while (low < height && arr[start] >= arr[++low])
;
// 从后向前找到第一个比基准值小的数值
while (height > start && arr[start] <= arr[--height])
;
// 没有交叉
if (low < height)
{
swap(&arr[low], &arr[height]);
}
else
{
// 交叉
break;
}
}
// 交换基准值与height
swap(&arr[start], &arr[height]);
quickSort(arr, start, height - 1);
quickSort(arr, height + 1, end);
}
// 打印
void print(int arr[], int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
// 数组
int arr[] = {9, -16, 30, 23, -30, -49, 25, 21, 30};
// 计算出数组的元素的个数
int length = sizeof(arr) / sizeof(int); // 9
quickSort(arr, 0, length - 1);
// 打印
print(arr, length);
return 0;
}
本章内容到此结束,欢迎你的关注