鄙人能力有限,可能会有错误和遗漏,有什么建议和意见望大家积极斧正,我会在开头鸣谢的。嘻嘻
线性结构
线性结构是我们经常使用的逻辑结构,是一切操作的基础,贯穿着我们写代码的操作的始终。
下面,我们针对这一结构做一个深入的讨论。
线性结构有两种实现方式,数组和链表,数组的操作相对比较容易,而且安全快速,所以如果没有特殊情况,对于一个任务首选数组实现。但是对于插入删除比较频繁或者事先并不确定规模的操作一般可以考虑链表。
顺序存储的数组
数组即顺序存储结构,我这里使用最简单的实现方式。
#include<stdio.h>
int main()
{
int arr[10] = {0};//确定最大规模是10
int last = 0;//这相当于尾指针,不过回避了指针的写法。
return 0;
}
首先,我按照我的习惯对数组进行了一次初始化(主要是因为我不放心)。
last的位置代表了数组“有值”的范围,后面虽然也有值0,但是在逻辑意义上,这些0是没有意义的,只是单纯的规定他们是零,如果你不愿意这么做,也可以,那这些没有意义的值就不一定是什么鬼东西了,不要随便去调用范围之外的值。
注意,这个所谓的有意义很重要,后面会有更多的解释。
然后注意last的位置,我设定为永远在没有值的位置,last 的值就代表了其数组的实际大小,你可以根据自己的喜好进行任意的变动,数据结构永远没有固定的写法,怎么舒服怎么来,怎么习惯怎么来,当然,在不牺牲大量效率的情况下。
这就是一个数组了,定义数组的任务完成了。
增删改查
然后就是对数组的所谓增删改查。首先是添加,当然,我们如果这么定义数组的话,很明显在结尾插入数据最节省时间。
(注意一定不要干出last越界的事情)
if(last < 10)
{
arr[last] = 10;
last++;//这两句也可以直接合并为arr[last++] = 10;
}
删除更简单,思考一下,既然l下标为last及last之后的数据没有意义,也就是说我只要给有意义的数据往前设置一下,那不就相当于将最后的数据删掉了嘛。
if(last > 0)
last--;//有意义的范围缩小了
所谓的栈就可以用这两个操作去实现
数组最大的有点就是支持随机存取,如果知道一个数据的下标,那查找这个数据将非常容易。
对于查找,这就涉及到了一个怎么查的问题,如果是无序的数组,也就是从前到后没有顺序上的逻辑关系,那么你怎么查都是O(n)的复杂度,所以,就不去想怎么查的问题了,从前到后找呗。
如果还不知道怎么遍历数组的小伙伴要好好康康
int i;
for (i = 0; i < 10; i++)
if (arr[i] == 50)//假设我想找50
{
printf("I find it! 50 is at %d\n", i);
break;//找到就可以跳出来了嗷,如果是写在函数里可以选择直接返回
}
if(i == 10)//所谓的“接着”上面的循环的i
printf("I can't find it!\n");
但是如果有顺序的数组,从前到后去一个一个找就显得很笨拙,具体做法后面再说。
但是数组有一个大毛病,就是他所谓的优点有时成为缺点,就是在插入删除上有遗漏,顺序存储给插入删除带来了不便,但是如果必须要插,那就插!又不是没有办法!
首先要设想,这是一群正在排队的人,如果Walker插队到第五个人前面,那第五个人往后的所有人必须都往后撤一步,但是,如果你让第五个人先撤,那一定会发生踩踏事故,必须要通知最后一个人往后撤,然后倒数第二个,倒数第三个,以此类推,直到第五个人往后撤一步之后,才能让Walker插进队里去。这个事实告诉我们,不要恶意插队,以免发生踩踏事故。
演示一下如何给Walker(假设值为100)插入到第五个人前面呢(就是让插入之后,Walker的下标为5)
int i;
if(last < 10)//时刻注意别越界,不过这一般是要自己提醒自己,这只是一个我的提醒
{
for(i = last - 1; i >= 5; i--)//last的位置没有值,last - 1才是最后一个人,从第五个人结束
arr[i + 1] = arr[i];//所谓的后撤一步,就是后一个承袭前一个。
arr[5] = 100;//假设Walker的值是100
last++;//最后一定不要忘了改变last 的值
}
看起来也不算太难哈,但是时间复杂度为O(n),在不需要太多的插删操作的时候还是相对可以接受的
可以在推广一下,假设Walker和他的孪生弟弟Walkor(假设值为99)都想插队,然后哥哥排在弟弟前边,使Walker的下标为5怎么办呢,只需要调整一下每个人后撤的距离就可以,每个人后撤两步就妥妥。
int i;
//if(last < 9)不提醒了,自生自灭吧!23333
for(i = last - 1; i >= 5; i--)
arr[i + 2] = arr[i];//每个人后撤两步就妥妥
arr[5] = 100;
arr[5 + 1] = 99;//为了方便推广到一般,就没写6
last += 2;
突然,有一位大哥怒吼:“Walker你给我滚出去!”,Walker就滚出去了,这就是所谓的删除操作,删除怎么办呢,其实是一个道理,但是要比添加要稍稍简单一点,只需要“不把Walker当人,所有Walker往后的同学向前跨一步,不就相当于给Walker挤出去了嘛,也就是删掉他了嘛”
int i;
for(i = 5 + 1; i < last; i++)//从第六个到最后一个
arr[i - 1] = arr[i];//每个人向前跨一步,即前边的承袭后边的
last--;//然后再将范围缩小
当然,如果叫孪生兄弟一起滚呢?
int i;
for(i = 5 + 2; i < last; i++)
arr[i - 2] = arr[i];
last -= 2;
好,Walker悄悄地走了,正如他悄悄地来。
他挥一挥衣袖,带走了一批程序员。
排序与查找(初识二分思想)
注意:接下来的操作可能对新手并不算友好。
然后是对数组的排序,因为数组可以随机存储,所以从中选出两个数比较大小就变得很简单,数组排序就变得相对快一些。
这里对排序做一个说明,如果没有特殊说明,我都是指从小到大排序,而且所谓的小是广义的顺序关系。广义的顺序关系就是假设我在一种东西内部规定一个前后关系,使得其内部随便两个东西都有前后关系,和自己一模一样的和自己的前后关系是任意的,但是和自己不一样的东西必须区分出前后关系,数学名曰“全序关系”。我这里的小于是指a <= b成立,所谓的大是指 a <= b不成立,这一点要注意。以整数为例小于是指小于等于,大于是真的大于。你可以根据需要任意改变合理的小于的定义,比如大于等于,字符串的大小关系,字符串的长短关系等等之类。
排序有很多方式,这里介绍其中几种。
首先是选择排序,就是假设从零下标开始,我往后找,给后面最小的数选出来放在零下标,然后再寻找位置1,再找一往后最小的,这样以此类推,每个数都是相对于他后面的数来说最小的,也就是从小到大排好了序。
这也是我最喜欢的排序方式(我只是说他比较简单,并没有说他是最好的)
int i, j;
for(i = 0; i < last; i++)
for(j = i + 1; j < last; j++)//最后会有一些多余的循环,这是不太追求效率的简便写法
if( ! (arr[i] <= arr[j]))//我这里回避了大于号以具有推广意义
{
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;//交换也可以直接写函数嗷,有大佬也可以用位运算符,这个随意
}
还有一些排序方法,例如冒泡排序,以上的排序方法时间复杂度都是O(n^2)的,属于比较稳定但是不太快的方法,下面介绍的算法是相对比较快的排序办法。
就是所谓的快速排序,这种排序采用了分治的思想。
现在设想这样一种情形,如果能有一种方法,使得我能让数组前边一部分的数小于后边一部分的数,那么我是不是就可以将数组排序的问题简化为排前边这一部分和排后一部分,然后,这两部分仍然使用这种办法,我是不是就可以将整个数组排序。
具体到用了什么办法能让数组前一部分小于后一部分呢,基本思想就是找俩人一个从前往后,另一个从后往前,对数组进行巡视,选取数组中的一个数R(一般选的都是第0个数),后边的人得保证巡视一回之后后边的数都应该比R大,前边的人呢,得保证前边的数比R小,前边的人先拿着R(假设我们选取的R就是arr[0]),后边的人从后往前和R作比较,顺序对,后边的人就巡查下一个,顺序不对,就将这个数和调R换次序,这样换好之后,R的后边的数都比R大,然后前边的人开始往前巡视,如果找到比R大的,就将这个数和R再调换,这样,之前检查过的数就都比R小,然后再换后边的人往前巡查,循环往复,就能使R前边的数都比R小,R后边的数都比R大,然后再从R所在位置将数组分开,再做一次类似的操作,数组就排好了。
从代码角度去看是这个样子。
void QuickSort(int arr[], int low, int high)//在[low, high]范围排序。
{
if(low >= high)
return;//作为终止条件,没有东西可以排了,算法结束
int i1 = low;
int i2 = high;//派两个人,一个从前往后巡查,另一个从后往前巡查
int t;
while(i1 < i2)//什么时候碰头什么时候算结束
{
while(i1 < i2 && arr[i1] <= arr[i2])
i2--;//后边的人先动身往前走,如果顺序一直对,那就一直往前走
t = arr[i1];
arr[i1] = arr[i2];
arr[i2] = t;//跳出循环了,说明俩人碰头了或者顺序不对了
//顺序不对了就换,俩人碰头了换一下子也没啥瓜系
//不断地调换,始终都是在和arr[low]作比较
while(i1 < i2 && arr[i1] <= arr[i2])
i1++;//同样的道理,后边的检验完了前边的人往后稍
t = arr[i1];
arr[i1] = arr[i2];
arr[i2] = t;
}
//如果碰头了,就说明已经排好了,整个过程O(n)
QuickSort(arr, low, i1 - 1);
QuickSort(arr, i2 + 1, high);//所谓的R已经排好,只需要排前后两部分就可
}
快速排序的时间复杂度在O(nlog(n)),比O(n^2)快很多,当数据很大时,两种方法的速度差异显著。
还有一种归并排序的排序方法,归并排序相比于快速排序进行了更严密的二分法,暂不做介绍。
当然这些算法建立在递归思想的基础之上,递归调用会有时间的损耗,所以转化成非递归方法可以加快速度,一般来说需要一个辅助栈协助完成才可以,等我更新到栈我会对此做详细介绍。
最后就是查找函数了,数组进行查找也是比较方便,如果是无序数组,那就最好从前往后进行寻找,但是有序数组则有更好的办法。
二分查找,时间复杂度O(log(n)),这就意味着十亿个数据那么大的数组其实也只需要最多30次的访问。
二分法大家想必都很了解,如果R已经确认在一个有序区间里,我只需要将R和区间中点进行比较,只考虑狭义的数的比较大小的话,如果比中点大,那一定在中点右面,如果比中点小,那么一定在中点左边,当然,如果等于中点,那么可以直接考虑找到了。基于这一思想,我们可以对其加以推广,我们取广义的小于含义,而且选取这样一个位置L,使得L位置的数据是小于R的最后一个元素(未必相等):
如果想在一个有序数组里查找某个数R,首先,我想先确认R确实有可能找到,以不至于白瞎功夫,所以我决定先让R和区间端点值进行比较,确认左端小于R而右端不小于R。如果到这就不满足,那么我们可以收手了。
好了,我确认R可能在区间里被找到,那我就选取中点结点M,如果M小于R,那我们可以缩小范围从M到右端,如果M不小于R,那我们可以缩小范围从左端到M,这样我们范围左端依然小于R,右端依然不小于R,然后继续寻找区间中点,这样,我们循环下去,当我们的区间小到只有两个数时,我们就可以确认,前一个数是小于R的,而后一个数是不小于R的,这样,我们就找到了小于R的最后一个数,即区间前一个数。
//假设我已经确保数据确实不小于左端而小于右端。
// 函数返回值即小于R的最后一个数
int UpperBound(int arr[], int low, int high, int R)//[low, high]内查找
{
if(high - low == 1)
return low;
int middle = low + (high - low) / 2;//防止high + low越界的中点写法
if(R <= arr[middle])
return UpperBound(arr, low, middle, R);
else
return UpperBound(arr, middle, high, R);
}
当然,你可以对这个算法稍作修改,变成各种各样自己想要的算法。
数组的一般操作大概就这样,下一期更新写链表