前言
今天在b站看了左神的第一节算法刷题课,受益挺多的。我大学学的是环工,但不想以后从事相关方面的工作,也没有跨专业考研的想法,本想着直接学编程就业,也面试过一些公司,但是无奈算法以及框架原理太过拉跨,一面之后就杳无音讯了。不过还好,毕业前夕参加了江西财经大学计算机系的第二学士学位选拔考试,顺利过渡为自己争取到了两年的学习时间,也算半个科班生了。当时项目写完之后,只剩下两个就要开始春招了,算法也是临时抱佛脚,东刷一题,西刷一题。没有任何提升。因此,我觉得在刷算法题之前一定要了解各种经典算法的基本实现原理以及概念,不然直接刷的话,我觉得是没有效果的,今天就总结一些排序算法以及二分查找,还有时间复杂度的概念。
时间复杂度
时间复杂度是在我看来是指一定的问题规模,需要运行的程序的次数,那么运行的次数一定可用一个多阶的多项式表示,那么我们用最高阶的项式来表示。比如说一个算法的复杂度可以用公式=a(N^2)+bN+c表示,那么算法复杂度就是O(N^2)。因为随着问题规模的增大,N的最高阶次项的增长速度是要快于低阶项和常数项。因此就可以省略。
经典的排序算法
先用c语言实现几个经典的排序算法
1.选择排序
#include <stdio.h>
int main() {
int arr[] = {25, 15, 7, 136, 9};
int tmp = 0;
for (int i=0;i<5;i++) {
for (int j=i+1;j<5;j++) {
if (arr[i] > arr[j]) {
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
for (int i=0;i<5;i++) {
printf("%d,", arr[i]);
}
}
原理很简单就是就是把每一位数都取出来然后和数组当中其他的进行比较,比当前的数小就替换他即可。 顺序排序的时间复杂度为O(N^2)。
2.冒泡排序
#include <stdio.h>
int main() {
int arr[] = {25, 15, 7, 136, 9, 5};
int tmp = 0;
int i,j;
int length = (int)(sizeof(arr)/sizeof(arr[0]));
for (i=0;i<length;i++) {
for (j=0;j<length-i-1;j++) {
if (arr[j] > arr[j+1]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
for (i=0;i<length;i++) {
printf("%d,", arr[i]);
}
}
冒泡排序的原理是相邻两位进行比较(本次是从左到右,按从小到大的顺序进行排序),如果左边的数比右边的数大交换两个数的位置,以此类推一直交换到最后一位,那么最后一位数就是最大的数。然后再从头开始,进行交换,那么倒数第二个数就是整个数列中倒数第二个小的数。一次类推,就可以得到一个有序数列。 冒泡排序的时间复杂度为O(N^2)。
3.插入排序
#include <stdio.h>
int main() {
int arr[] = {25, 15, 7, 136, 9, 5};
int tmp = 0;
int i,j;
int length = (int)(sizeof(arr)/sizeof(arr[0]));
for (i=1;i<length;i++) {
for (j=i;j>=1;j--) {
if (arr[j] < arr[j-1]) {
tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
}
}
}
for (i=0;i<length;i++) {
printf("%d,", arr[i]);
}
}
插入排序的做法就是从数组第二个位置开始,与数组第1位进行比较,如果第二位比它小交换位置,那么0,1位置就实现了有序,接下来是将数组当中第三个位置的数与第二位进行比较,如果比它小就交换到第二个位置,再和第一位进行比较,如果还比第一位小,就再交换到第一个位置即可。依次类推。 插入排序的时间复杂度为O(N^2)。
4.二分查找
我们看下面这一道题目 给定一个有序数组
int arr[] = {1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5};
求这个数组中大于等于3的索引值最小的数组元素。
这道题的思路如:因为整个数组是有序的,所以我们可以利用二分法将大于某特定数的数一定是在该特定的右边,小于该特定数的数一定是在其右边。因此我们可以先判断该数列中间位置的数,判断其是否大于等于3,如果大于等于3,就在该中位数的左边寻找,如果小于的话就在该中位数的右边寻找。
#include <stdio.h>
int main() {
int arr[] = {1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5};
int length = (sizeof(arr)/sizeof(arr[0]));
int begin = 0,end =length-1;
int mid = 0;
while (begin <= end) {
mid = (begin+end)/2;
if (arr[mid] >= 3) {
end = mid - 1;
}
else {
begin = mid + 1;
}
}
printf("大于等于3最近的数是第%d位,值为%d", mid+1, arr[mid]);
}
先计算出数组的长度,然后再定义两个变量,可以理解为2个指针,一个为头指针一个为尾指针。用于记录二分判断之后数组的范围,先运用一个while循环,当头指针的值大于等于尾指针,说明二分数组的长度已经达到极限,就停止循环。在循环体内,如果中位值大于等于3,将中位数的数组索引值减1然后赋给尾指针,这样二分的数组就是中位数右边的数组,如果小于3,就将中位值加1赋给头指针,这样下次进行二分的数组就是,中位数右边的数组,等到二分结束得到的数组元素就是大于等于3索引值最小的数。
从这里我们可以总结一下二分法的基本写法
1.获取数组的长度。 2.定义两个指针变量,分别指向数组的头部和尾部。 3.利用while循环,等到头部指针变量值小于等于尾部指针变量值时结束循环。
但是,有些人可能会认为二分法只能用于有序的数组,其实这是错误的,无序的情况下有时用二分法解决,可以降低时间复杂度。比如求一个局部最小值,题目如下:
局部最小的定义如下: 对一个长度为N的数组a,数组当中的相邻元素都不相等。如果存在a[0]<a[1]或a[N-2]<a[N-1],或者当2<i<N-2时,有a[i-1]>a[i]且a[i+1]>a[i]都可以称为该数组存在局部最小。
由此,设一个数组,长度为N,其中一个局部最小值,要求时间复杂度好于O(N)。
根据题目我们可以这样思考,假设数组0位置不存在局部最小,N-1位置也不存在局部最小那么0到1位置是单调递减的,N-2到N-1位置是单调递增的,由于数组当中相邻位置元素的值是不相等的,所以一定会存在一条数值波动曲线,那么头部曲线是单调递减的,尾部曲线是单调递增的,那么中间的数当中,一定存在一个极小值的拐点,即局部最小的位置。那么我们就可以运用二分法,时间复杂度为O(log2N)。 算法如下:
#include <stdio.h>
int main() {
int arr[] = {100, 50, 43, 66, 67, 78, 85, 84, 83};
int length = (sizeof(arr)/sizeof(arr[0]));
int begin = 0,end =length-1, minIndex = 0;
int mid = 0;
while (begin <= end) {
if (arr[0] < arr[1]) {
minIndex = 0;
break;
}else if (arr[length-2] < arr[length-1]) {
minIndex = length-2;
break;
}
mid = (begin+end)/2;
if (arr[mid] < arr[mid-1] and arr[mid] <arr[mid+1]) {
minIndex = mid;
break;
}
else {
if (arr[mid] > arr[mid-1]) {
end = mid;
}else {
begin = mid;
}
}
}
printf("其中一个局部最小是第%d位,值为%d", minIndex+1, arr[minIndex]);
}
所以二分法不是只适用于有序的数组,有时我们需要看题目的条件是否满足二分查找的类型,再做定夺。
二分查找的时间复杂度为O(log2N)。