第一讲:数据结构和算法绪论

1.什么是数据结构?

数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。

数据结构就是关系,没错,就是数据元素相互之间存在的一种或多种特定关系的集合。

数据结构 + 算法 = 程序设计

2.数据结构分为哪两种结构?

(1)逻辑结构:

数据对象中数据元素之间的相互关系,也是后续的重点。

(2)物理结构:

数据的逻辑结构在计算机中的存储形式。

3.四大逻辑结构

①集合结构中的数据元素除了同属于一个集合外,之间没有啥“不三不四”的关系。
②线性结构中的数据元素之间是一对一的关系。
③树形结构中的数据元素之间存在一种一对多的层次关系。
④图形结构的数据元素是多对多的关系。就像人类社会,他和她,她又和他,产生了不可言传的关系~

4.两大物理结构

物理结构说白了就是:

如何把数据元素存储到计算机的存储器中(硬盘,软盘,光盘)

(1)顺序存储:

把数据元素存放在地址连续的存储单元里,数据间的逻辑关系和物理关系是一致的。

就像这个数组:

var a = [1,2,3,4,5];

从顺序存储结构我们想到了日常生活中我们的排队,有木有?

但现实生活中,我们发觉也并不完全如此。

例如:

有人排着排着她内急,她要被迫离开队伍去上洗手间,还有人不遵守基本基本道德规范他插队,这些情况会大破存储存储结构的基本原则。

面对这样时常要变化的结构,顺序存储是不科学滴~

那么就该让链式存储结构露面了!

(2)链式存储结构:

是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。

说白了就是:

类似现在的排号系统,先领一个号码,在你被叫到之前,你想干啥干啥,只要叫到你的时候,在现场就可以

既然是数据结构,那么还是要有“逻辑约束”,因此需要用一个指针存放数据元素的地址。

这样子通过地址就可以找到相关联数据元素的位置。

第二讲:谈谈算法

1.为什么数据结构和算法要放在一起讲?

打个比方,其实数据结构和算法的关系就比好基友是一辈子的关系。他们患难见真情,他们生死不相弃,他们荣辱与共,他们一生情一辈子……

说白了:

数据结构是锁,必须要有一把钥匙,即算法,插进去,才能开启一个神奇的事情!

小学学过珠算的应该很有印象,每天加法运算敲得手指都快断了就算:

1+2+…+99+100

在地球的某个地方,出现了一位神童,他为了早点回家吃午饭,发明了一个牛x的算法:
等差数列求和

(首项 + 末项)x项数 /2
数学表达:1+2+3+4+……+ n = n (n+1) /2

这位神童,就是后来家喻户晓的 Gauss (高斯),随着你的学术生涯提升,你会接二连三被他“搞死”

有些编程基础的,就会得瑟:
我会for循环,这么点数字能难得倒我?!

三下五除二,写下这段代码:

int i, sum = 0, n = 100;
for(i=1; i <= n; i++)
{
sum = sum + i;
}
printf(“%d”, sum);

祝贺你,写出来一段很有厉害(没卵用)的代码!

看看写的高斯算法代码:

int i, sum = 0, n = 100;
sum = (1+n)*n/2;
printf(“%d”, sum);

单纯的你可能会说:

以计算机CPU的运算神速,两个算法都可以秒杀解决掉求和问题!但是,如果我们把条件换成1加到1千万,或者1加到1千亿,差距就可想而知了,甚至人脑都可以比电脑计算得快了。

说白了:

当你总资产只有几块时,几毛钱都很重要

2.什么是算法?

官方定义:(不用太认真)

算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

其实:

算法就是你泡妞儿的技巧和方式。当你是一个单纯的以为写封情书,并告诉妹子,“我爱你”,就能拿啥的单纯boy,注定单身撸代码。假如你是高傲,孤冷,套路很多的老司机,注定彩旗飘飘~

就像没有药可以包治百病一样
一个问题可以由多个算法解决
一个算法也不可能具有通解所有问题的能力

3.算法五大基本特征

输入。算法具有零个或多个输入的接口。
尽管对于绝大多数算法来说,输入参数都是必要的。但是有些时候,像打印“I love fishc.com”,就不需要啥参数啦。

void print()
{
printf(“I love fishc.com\n”);
}

输出。算法至少有一个或多个输出。

算法是一定要输出的,不需要它输出,那你要这个算法来干啥?
就像光吃,不拉,怎么能受得了呢?输出的形式可以是打印形式输出,也可以是返回一个值或多个值等。

有穷性。指算法在执行有限的步骤之后,自动结束而不会出现无限循环。

并且每一个步骤在可接受的时间内完成。
一个永远都不会结束的算法,我们还要他来干啥?(忽略装x的理由)

确定性。算法的每一个步骤都具有确定的含义,不会出现二义性。

算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。算法的每个步骤都应该被精确定义而无歧义。

一个你都不知道在干啥的算法,我们还要他干啥?(忽略你不懂的大牛算法,总有人懂)

可行性。算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。

一个自相矛盾,跑不了的算法,我们还要他干啥?

4.算法设计5大特性
①正确性

算法程序没有语法错误:
意味着你编写的算法,从程序语法规定上将不能有问题。

就像循环,你不能写出很没边的条件

for(var i=0; i>1 ; i++){}

算法程序对于合法输入能够产生满足要求的输出:
意味着,正确的输入一定要输出符合要求的结果。

算法程序对于非法输入能够产生满足规格的说明:
意味着,例如你要求输入数字,有人输入字符串,算法一定要报错。

算法程序对于故意刁难的测试输入都有满足要求的输出结果:
意味着,假如你的算法只需要用到百万量级,有的人,非要输入千万量级。

那么你的算法,也要能正确输出他该输出的结果。

②可读性
算法设计另一目的:是为了便于阅读、理解和交流。

写代码的目的:

一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读和自己日后“阅读”和“修改”。

③健壮性

当输入数据不合法时,算法也能做出相关处理,而不是产生异常、崩溃或莫名其妙的结果。

不管你怎么折腾,算法就是可以正常运行。

④时间效率

同样的一段代码,你的算法可以比别人跑得快,这就是优势。

往往是高手过招的关键点!

⑤存储量

同样的一段代码,你的算法比别人占用内存少,这就是优势。

往往是高手过招的关键点!

第三讲 时间复杂度和空间复杂度1

1.两大度量算法方法

①事后统计

因为计算机都有计时功能,所以可以通过计时比快慢的方法来衡量算法效率。这种方法主要是通过设计好的测试程序和数据。利用计算机计时器对不同算法编制的程序的运行时间进行比较。但这种方法显然是有缺陷的:必须依据算法事先编制好测试程序,通常需要花费大量时间和精力,完了发觉测试的是糟糕的算法,那不是功亏一篑?赔了娘子又折兵?不同测试环境差别不是一般的大!

② 事先分析

在计算机程序编写前,依据统计方法对算法进行估算。经过总结,我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:1. 算法采用的策略,方案2. 编译产生的代码质量3. 问题的输入规模4. 机器执行指令的速度由此可见,抛开这些与计算机硬件、软件有关的因素。一个程序的运行时间依赖于算法的好坏和问题的输入规模。
(所谓的问题输入规模是指输入量的多少)

2.高斯(搞死)等差数列实战

第一种算法:

int i, sum = 0, n = 100;   // 执行1次
for( i=1; i <= n; i++ )    // 执行了n+1次
{
sum = sum + i;          // 执行n次
}

不同的变量执行次数不一样,核心算法循环了n次。

第二种算法:

int sum = 0, n = 100;     // 执行1次
sum = (1+n)*n/2;          // 执行1次

虽然只标注了1次,乍一看很牛x。
但是仔细想一想,那个n=100咋来的。。。

暂时抛开这个问题,只从这两段代码结构上来讲,显然第二种算法效率更高。只从执行次数维度来看:

第一种算法执行了1+(n+1)+n=2n+2次。
第二种算法,是1+1=2次

3.深入剖析

一段代码:

int i, j, x=0, sum=0, n=100;
for( i=1; i <= n; i++ )
{
for( j=1; j <= n; j++ )
{
x++;
sum = sum + x;
}
}

复制代码

这个例子中,循环条件i从1到100,每次都要让j循环100次。
如果非常较真的研究总共精确执行次数,那是非常累的。

另一方面,我们研究算法的复杂度,侧重的是:
研究算法随着输入规模扩大增长量的一个抽象整体。

而不是:精确地定位需要执行多少次
因为如果这样的话,我们就又得考虑回编译器优化等问题。

然后如同掉进一个“兔子洞”,然后就永远也没有然后了!
所以,对于刚才例子的算法,我们可以果断判定需要执行100^2次。(外层从1循环到100,每一次内层也是从1到100,100*100)

我们不关心编写程序所用的语言是什么,也不关心这些程序将跑在什么样的计算机上。我们只关心它所实现的算法。

4.效率的度量方法

在上面的例子中,我们通过度量“实现算法”,来衡量效率问题。

这样,不计那些循环索引的递增和循环终止条件、变量声明、打印结果等操作。
最终,在分析程序的运行时间时,最重要的是:把程序看成是独立于程序设计语言的算法或一系列步骤。

我们在分析一个算法的运行时间时,重要的是:
把基本操作的数量和输入模式关联起来。

横轴代表输入值,纵轴代表输出值,三条不同的函数,对应不同的曲线。
当输入量小的时候,三个函数(算法)增长量相差无几。
但随着输入值越大,n*n函数增长量瞬间秒杀其他两个。
这个通过操作数量和输入模式来分析算法的运行时间。

5.函数的渐进增长

当n=1时,算法A1效率不如算法B1,当n=2时,两者效率相同;

当n>2时,算法A1就开始优于算法B1了,随着n的继续增加,算法A1比算法B1逐步拉大差距。

所以总体上算法A1比算法B1优秀。

6.渐进增长定义

渐进增长:
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大。

那么,我们说f(n)的增长渐近快于g(n)

从刚才的对比中我们还发现,随着n的增大,后面的+3和+1其实是不影响最终的算法变化曲线的。

例如算法A2,B2,在图中他们压根儿被覆盖了。

所以,我们可以忽略这些加法常数。(维度不一样,攻击效果不一样)

最高次项的指数大的,函数随着n的增长,结果也会变得增长特别快。判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高项)的阶数。

第四讲 时间复杂度和空间复杂度2

1.算法时间复杂度

官方定义:

在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。

重点:

执行次数==时间

这样用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。

2.分析大O阶

如何分析一个算法的时间复杂度呢?
即如何推导大O阶呢?

我们给大家整理了以下攻略:

用常数1取代运行时间中的所有加法常数。在修改后的运行次数函数中,只保留最高阶项。如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的最后结果就是大O阶。

3.实例搞懂大O

①常数阶

从高级语言维度来看(汇编级别就不考虑了),这段代码是O(8)?int sum = 0, n = 100;printf(“I love fishc.com\n”);printf(“I love Fishc.com\n”);printf(“I love fishC.com\n”);printf(“I love fIshc.com\n”);printf(“I love FishC.com\n”);printf(“I love fishc.com\n”);sum = (1+n)*n/2;O(8)?这是初学者常常犯的错误,总认为有多少条语句就有多少。分析下,按照我们的概念“T(n)是关于问题规模n的函数”纵观整段代码,只有最后一条语句涉及到n变量。其余几条只是输出语句,跟运算没毛线关系~攻略第一条也说明了所有加法常数给他个O(1)即可

②线性阶

线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。

int i , n = 100, sum = 0;
for( i=0; i < n; i++ )
{
sum = sum + i;
}

它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。

③平方阶

平方阶,就是线性阶的线性阶(a*a)

即,循环嵌套:

int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=0; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}

n等于100,也就是说外层循环每执行一次,内层循环就执行100次。

那总共程序想要从这两个循环出来,需要执行100*100次,也就是n的平方。所以这段代码的时间复杂度为O(n^2)。

有多少层循环嵌套,时间复杂度就是O(n^层)?

建议你千万不要这么想!

坑来了:

int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=i; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}

注意第四行的j赋值。

分析下,由于当i=0时,内循环执行了n次,当i=1时,内循环则执行n-1次……

当i=n-1时,内循环执行1次,所以总的执行次数应该是:

n+(n-1)+(n-2)+…+1 = n(n+1)/2

大家还记得这个公式吧?

恩恩,没错啦,就是高斯(搞死)先生发明的算法丫。

那咱理解后可以继续,n(n+1)/2 = n^2/2+n/2

用我们推导大O的攻略:
第一条忽略,因为没有常数相加。
第二条只保留最高项,所以n/2这项去掉。
第三条,去除与最高项相乘的常数,最终得O(n^2)。

④对数阶

忘记了,也没事,咱分析的是程序为主,而不是数学为主,不怕。

int i = 1, n = 100;
while( i < n )
{
i = i * 2;
}

i每次循环,都会乘2.
假设有x个2相乘后大于或等于100,则会退出循环。
于是由2^x = n得到x = log(2)n,所以这个循环的时间复杂度为O(logn)。

其实理解大O推导不算难,难的是对数列的一些相关运算。
这更多的是考察你的数学知识和能力。

第五讲 时间复杂度和空间复杂度3

1.超级实战!

n++;
function(n);
for(i=0; i < n; i++) {
function(i);
}void function(int count) {
int j;
for(j=count; j < n; j++) {printf(“%d”, j);
}
}for(i=0; i < n; i++) {for(j=i; j < n; j++) {printf(“%d”, j);}
}

请问这段代码的时间复杂度是?
事实上,这和之前我们讲解平方阶的时候举的第二个例子一样。

前景回顾:

function内部的循环次数随count的增加(接近n)而减少.

所以根据游戏攻略算法的时间复杂度为O(n^2)。

答案揭晓:

2.装X术语总结


常用的时间复杂度所耗费的时间总结:

O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

3.最坏情况与平均情况

从心理学角度讲,每个人对将来要发生的事情都会有一个预期。譬如看半杯水,有人会说:哇哦,还有半杯哦!但有人就会失望的说:天,只有半杯了。

一般人常出于一种对未来失败的担忧,而在预期的时候趋向做最坏打算。这样,即使最糟糕的结果出现,当事人也有了心理准备,比较容易接受结果,假如结局并未出现最坏的状况,这也会使人更加快乐,瞧,事情发展的还不错嘛!

算法的分析也是类似,我们查找一个有n个随机数字数组中的某个数字。
最好的情况是第一个数字就是,那么算法的时间复杂度为O(1)。
但也有可能这个数字就在最后一个位置,那么时间复杂度为O(n)。
平均运行时间是期望的运行时间。
最坏运行时间是一种保证(下限)。

在应用中,这是一种最重要的需求,通常除非特别指定: 我们提到的运行时间都是最坏情况的运行时间

4.算法的空间复杂度

我们在写代码时,完全可以用空间来换去时间。

第一种方法:

要判断某年是不是闰年,你可能会花一点心思来写一个算法,每给一个年份,就可以通过这个算法计算得到是否闰年的结果。

秀一段Js的算法实现:

< script>
function isLeapYear(year) {  return (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);  }
< /script>

第二种方法:

事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字对应,如果是闰年,则此数组元素的值是1,如果不是元素的值则为0。

这样,所谓的判断某一年是否为闰年就变成了查找这个数组某一个元素的值的问题。

第一种方法相比起第二种来说很明显非常节省空间。
但每一次查询都需要经过一系列的计算才能知道是否为闰年。

第二种方法虽然需要在内存里存储2050个元素的数组。
但是每次查询只需要一次索引判断即可。

这就是通过一笔空间上的开销来换取计算时间开销的小技巧。

到底哪一种方法好?
其实还是要看你用在什么地方。

算法的空间复杂度的计算公式记作:

S(n)=O(f(n))

其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

老教授定义:

算法的空间复杂度通过计算算法所需的存储空间实现,通常,我们都是用“时间复杂度”来指运行时间的需求,是用“空间复杂度”指空间需求。

当直接要让我们求“复杂度”时,通常指的是时间复杂度。

第六讲 线性表1

1.感受线性表

小甲鱼组织鱼油们外出野营。

因为咱鱼油队伍强大,要小甲鱼记住每位鱼油并区分还真不是一件容易的事。

所以咱想到了一个办法,让大家按照一个约定排成一队,以后大家要怎么记住自己的位置呢?

没错,大家只需要:
记住自己前边的鱼油即可。

小甲鱼也可以很快的清点人数,万一有人走丢,也能在最快时间内知道是谁不见了。

因为只需要问哪位鱼油的“前驱”不见啦即可。

线性表,从刚才小甲鱼的描述中我们可以很容易感受到是像排队一样,具有线一样性质的结构。

2.线性表(List)的定义:

由零个或多个数据元素组成的有限序列。

数学语言来进行定义:

若将线性表记为(a1,…,ai-1,ai,ai+1,…an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。

强调几个关键的地方:

首先它是一个序列,也就是说元素之间是有个先来后到的。若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继。线性表强调是有限的,事实上无论计算机发展到多强大,它所处理的元素都是有限的。

3.深入理解“抽象”

定义:

指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。

抽象是一种思考问题的方式,它隐藏了繁杂的细节。

我们对已有的数据类型进行抽象,就有了抽象数据类型。

4.数据类型:

是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。例如很多编程语言的整型,浮点型,字符型这些指的就是数据类型。

当年那些设计计算机语言的前辈,为什么会考虑到数据类型呢?

比如,大家都需要住房子,也都希望房子越大越好。
但显然:
没有多少钱的话考虑房子是没啥意义的。

于是商品房就出现了各种各样的房型,有别墅的,有错层的,有单间的…
满足了大家的不同需求。

同样:
在计算机中,内存也不是无限大的。

你要计算入1+1=2这样的整型数字的加减乘除运算,显然不需要开辟很大的内存空间。

而如果要计算1.23456789+2.987654321这样带大量小数的,就需要开辟比较大的空间才存放的下。

于是计算机的研究者们就考虑,要对数据类型进行分类!
分出多种数据类型来适合各种不同的计算条件差异。

C语言中,按照取值的不同,数据类型可以分为两类:

原子类型:不可以再分解的基本类型,例如整型、浮点型、字符型等。结构类型:由若干个类型组合而成,是可以再分解的,例如整型数组是由若干整型数据组成的。

5.抽象数据类型

抽象数据类型(Abstract Data Type,ADT),定义:

一个数学模型及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。

比如1+1=2这样一个操作:

在不同CPU的处理上可能不一样,但由于其定义的数学特性相同。

所以在计算机编程者看来,它们都是相同的。

结论:

抽象数据类型不仅仅指那些已经定义并实现的数据类型,还可以是计算机编程者在设计软件程序时自己定义的数据类型。

一个3D游戏中,要定位角色的位置,那么总会出现x,y,z三个整型数据组合在一起的坐标。

我们就可以定义一个point的抽象数据类型,它拥有x,y,z三个整型变量。

好处:
将角色的位置操作转换为可控的数学描述

为了便于在之后的讲解中对抽象数据类型进行规范的描述。
我们给出了描述抽象数据类型的标准格式:(还不抄下来! )

ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作
endADT

第七讲 线性表2

①还是回到小甲鱼组织大家春游的例子,小甲鱼把鱼油们按照规律安排成一队。并且是长期使用这样的顺序排队,大家只需要记住自己的前驱鱼油就可以了。

那么这个考虑和安排的过程其实就是:线性表的创建和初始化过程。

② 一开始小甲鱼没经验呀,把鱼油们按照名字第一个字母的规律排队。(也算是一种算法哈)

发现排到最后有的高有的矮,导致队伍很难看,于是让鱼油们解散重新按照从矮到高排。

上面的过程其实就是:线性表重置为空表的操作过程。

③接着给大家描述下删除数据

排好队后,尼玛,发现黑夜童鞋由于昨晚吃太多麻辣烫,今天闹肚子来不了春游。那么由原来排在黑夜后边(不许想歪哦 )的不二开始往前挪。

④当然有删除数据就有插入数据

黑夜童鞋下午肚子康复了,又回来了。黑夜童鞋说记得是排在风介后边的,所以让从风介后边的康小泡开始往后退一个位置。黑夜就可以插回自己的位置上了。

⑤当然还可以根据位序(位置顺序)得到数据

噢,我们活动开始了不久,隔壁来了春田花花旅游团都是美眉啊,各种调戏有木有?然后小甲鱼被她们的导游投诉了有木有?!导游说你们队伍里第八个那个家伙老是调戏我们队里的小花,他叫什么名字?小甲鱼查了下名单说,噢,他叫迷途。

剩下还有几个概念,大家可以充分发挥脑洞,通过生活例子把概念列举出来,会加快你掌握这个概念哦~

2.概念总结

Data:

线性表的数据对象集合为{a1,a2,…,an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素。除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。

Operation:

InitList(*L): 初始化操作,建立一个空的线性表L。ListEmpty(L): 判断线性表是否为空表,若线性表为空,返回true,否则返回false。ClearList(*L): 将线性表清空。GetElem(L,i,*e): 将线性表L中的第i个位置元素值返回给e。LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。ListInsert(*L,i,e): 在线性表L中第i个位置插入新元素e。ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值。ListLength(L): 返回线性表L的元素个数。

endADT:

对于不同的应用,线性表的基本操作是不同的。上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作。完全可以用这些基本操作的组合来实现。

3.例子

比如要实现两个线性表A、B的并集操作,即要使得集合:

A=A∪B

解释下“A=A∪B”:

说白了,就是把存在集合B中,但不存在集合A中的元素,插入到A中即可。

其实仔细思考一下,我们只需要循环遍历集合B中的每个元素。

判断当前元素是否存在A中,若不存在,则插入A中即可。

综合分析,我们需要运用到几个基本的操作组合即可:

ListLength(L);
GetElem(L,i,*e);
LocateElem(L,e);
ListInsert(*L,i,e);

来一段代码过过瘾,加深下理解:

//La表示A集合,Lb表示B集合
// union代表并集,L代表线性表
void unionL(List *La, list Lb)
// List *La 带指针,List Lb不带
{int La_len, Lb_len, i;ElemType e;// 分别求两个线性表长度,实际是一样哦La_len = ListLength(*La);Lb_len = ListLength(Lb);// 迭代获取线性表中i个元素,从1开始是为了给你看for( i=1; i <= Lb_len; i++ ){GetElem(Lb, i, &e);// 判断有没有if( !LocateElem(*La, e) ){// 没有就插入ListInsert(La, ++La_len, e);}}
}

第八讲 线性表3

1.存储结构

线性表有两种物理存储结构:顺序存储结构和链式存储结构。

这次主要介绍顺序存储结构,链式存储结构请看下一讲~

2.顺序存储结构:

用一段地址连续的存储单元依次存储线性表的数据元素。

线性表(a1,a2,…,an)的顺序存储如下:
上边的图片大家想到了什么?

没错,就是在各种编程语言中,都会有的:数组(Array)

物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式。把一定的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中。

大家喜欢听小甲鱼拿现实的栗子来介绍概念。
哈,就例如小甲鱼带着咱的鱼C旅游团去看电影,看那个叫3D*蒲团有木有?!

哎哟,去到电影院人山人海,但妹子的身影还是难以逃过小甲鱼的火眼精金。所以小甲鱼猛地带领众鱼油一把夺下了妹子旁边的一排位置。。。。。。话说小甲鱼拿下第一个位置后(我说没有私心,你信吗 ),鱼油们果断依次坐下,这样子我们就建立了一个线性表。

由此可见,这小甲鱼夺下的第一个位置是非常关键滴!

如果妹子身旁的位置被其他人占据了,小甲鱼就不能代替大家问到妹子的联系方式了!

可现实中总会有些不按部就班来的,例如黑夜喜欢乱吃零食,所以他又拉肚子了。。。

但是黑夜买票了,所以他的位置就算空着,也没有人会坐上去。(忽略没有素质的人,随便换座哈)

顺序存储的结构代码:

#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length;    // 线性表当前长度
} SqList;

大家看到了,这里我们封装了一个结构。

事实上就是对数组进行封装,增加了个当前长度的变量罢了。

总结:

存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置。线性表的最大存储容量:数组的长度MaxSize。线性表的当前长度:length。

注意,数组的长度与线性表的当前长度需要区分一下:

数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会变化的。

3.地址计算方法

假设ElemType占用的是c个存储单元(字节)。
那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是:

LOC(ai+1) = LOC(ai) + c

LOC表示获得存储位置的函数
所以对于第i个数据元素ai的存储位置可以由a1推算得出:

LOC(ai) = LOC(a1) + (i-1)*c

结合下图来理解:

通过这个公式,我们可以随时计算出线性表中任意位置的地址。
不管它是第一个还是最后一个,都是相同的时间。
那么它的存储时间性能当然就为0(1),我们通常称为随机存储结构。

4.获得元素操作

学习代码:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0typedef int Status;// Status 是函数的类型,其值是函数结果状态代码,如OK
// 初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值
Status GetElem(SqList L, int i, ElemType *e)
{if( L.length==0 || i<1 || i>L.length ){return ERROR;}*e = L.data[i-1];return OK;
}

注意这里返回值类型Status是一个整型,约定返回1代表OK,返回0代表ERROR。

今后还会出现,也是使用同样的约定,不再详述。

线性表的顺序存储结构具有随机存储结构的特点,时间复杂度为O(1)。

小甲鱼在电影院成功的博得妹子的欢心,妹子表示愿意加入我们鱼C旅游团。大家狠兴奋丫有木有!因为大家都希望:小甲鱼把妹子排到自己身边。

但是我们要按我们定下的规则来排:
5.按身高顺序

所以插入算法的思路:

如果插入位置不合理,抛出异常;如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量;从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;将要插入元素填入位置i处;线性表长+1。

实现代码:

// 初始条件:顺序线性表L已存在,1<= i <= ListLength(L)
// 操作结果:在L中第i个位置之前插入新的数据元素e,L长度+1Status ListInsert(SqList *L, int i, ElemType e)
{int k;if( L->length == MAXSIZE )  // 顺序线性表已经满了{return ERROR;}if( i<1 || i>L->length+1)   // 当i不在范围内{return ERROR;}if( i <= L->length )   // 若插入数据位置不在表尾{// 将要插入位置后数据元素向后移动一位for( k=L->length-1; k >= i-1; k-- ){L->data[k+1] = L->data[k];}}L->data[i-1] = e;  // 将新元素插入L->length++;return OK;
}

第九讲 线性表4

1.删除操作

删除算法的思路

如果删除位置不合理,抛出异常;取出删除元素;从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;表长-1。

代码实现

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */
Status ListDelete(SqList *L, int i, ElemType *e)
{int k;// 判断是否是空表if( L->length == 0 ){return ERROR;}// 判断删除元素位置是否不合法if( i<1 || i>L->length ){return ERROR;}// 数组从0开始,i位置对应索引-1,记住*e = L->data[i-1];if( i < L->length ){// 循环,向前移动1位for( k=i; k < L->length; k++ ){L->data[k-1] = L->data[k];}}// 删除一次成功,表长减1L->length--;return OK;
}

2.分析插入和删除的时间复杂度

最好的情况:

插入和删除操作刚好要求在最后一个位置操作,因为不需要移动任何元素,所以此时的时间复杂度为O(1)。

最坏的情况:

如果要插入和删除的位置是第一个元素,那就意味着要移动所有的元素向后或者向前,所以这个时间复杂度为O(n)。

平均情况:

中间值O((n-1)/2) -> O(n/2 - 2) ->平均情况复杂度简化:O(n)。

3.顺序存储结构的优缺点

线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。

而在插入或删除时,时间复杂度都是O(n)。

这就说明:

它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用。

优点:

◆无须为表示表中元素之间的逻辑关系而增加额外的存储空间。◆可以快速地存取表中任意位置的元素。

缺点:

插入和删除操作需要移动大量元素。当线性表长度变化较大时,难以确定存储空间的容量。容易造成存储空间的“碎片”。

4.链式存储结构

前面我们讲的线性表的顺序存储结构,它最大的缺点就是:

插入和删除时需要移动大量元素,这显然就需要耗费时间。

那我们能不能针对这个缺陷或者说遗憾提出解决的方法呢?
要解决这个问题,我们就得考虑一下导致这个问题的原因!
为什么当插入和删除时,就要移动大量的元素?

原因就在于:

相邻两元素的存储位置也具有邻居关系,它们在内存中的位置是紧挨着的,中间没有间隙,当然就无法快速插入和删除。

经过叽叽呱呱的讨论之后,我们派出几个童鞋跟大家分享一下思路。

A童鞋:

让当中每个元素之间都留有一个空位置,这样要插入一个元素时,就不至于要移动了。可一个空位置如何解决多个相同位置插入数据的问题呢?所以这个想法显然不行。

B童鞋:

那就让当中每个元素之间都留足够多的位置,根据实际情况制定空隙大小,比如每个元素间留10个空位。那么问题就显而易见了,造成资源的极大浪费,并且在同一个位置插入11次也不是不可能。

C童鞋:

反正要在相邻元素间留多少空间都是有可能不够的,那不如干脆不要考虑相邻位置这个问题了。哪里有空位就放在哪里,记得在小甲鱼老湿的《零基础入门学习C语言》中讲到的指针刚好可以派上用场。每个元素都用一个位置来存放指向下一个元素的位置的指针。这样子从第一个元素可以找到第二个元素,第二个元素可以找到第三个元素。依此类推,所有的元素我们就都可以通过遍历而找到了。

好,太棒了,这个想法灰常好!掌声鼓励!

链式存储结构定义

线性表的链式存储结构的特点:

用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。

比起顺序存储结构每个数据元素只需要存储一个位置就可以了。

现在链式存储结构中:

除了要存储数据元素信息外,还要存储它的后继元素的存储地址(指针)。

也就是说除了存储其本身的信息外,还需额外存储一个指示其直接后继的存储位置的信息。

数据域:

存储数据元素信息的域

指针域:

存储直接后继位置的域。

指针或域:

指针域中存储的信息。

结点:

数据域和指针域这两部分信息组成数据元素也称为存储映像。

链式存储结构:

n个结点链接成一个链表,即为线性表(a1, a2, a3, …, an)因为此链表的每个结点中只包含一个指针域,所以叫做单链表。

5.单链表

对于线性表来说,总得有个头有个尾,链表也不例外。

我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)。

第十讲 线性表5

1.头指针与头结点的异同

上节课我们提到了,头结点的数据域一般不存储任何信息。
谁叫它是第一个呢,有这个特权。

拿个小旗子即可:

既然头结点的数据域不存储任何信息,那么头指针和头结点又有何异同呢?

头指针:

头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。无论链表是否为空,头指针均不为空。头指针是链表的必要元素。

头节点:

头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。头结点不一定是链表的必须要素。

2.存储结构

单链表图例:
空链表图例:
我们在C语言中可以用结构指针来描述单链表。

typedef struct Node
{
ElemType data;      // 数据域
struct Node* Next;  // 指针域
} Node;
typedef struct Node* LinkList;

我们看到:
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。

代码实现

假设p是指向线性表第i个元素的指针。
则该结点ai的数据域我们可以用p->data的值是一个数据元素。

结点ai的指针域可以用p->next来表示。
p->next的值是一个指针。

那么p->next指向谁呢?

当然指向第i+1个元素!
也就是指向ai+1的指针。

问题:

如果p->data = ai,那么p->next->data = ?

答案:

p->next->data = ai+1。

3.单链表的读取
在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。

但在单链表中,由于第i个元素到底在哪?
我们压根儿没办法一开始就知道,必须得从第一个结点开始挨个儿找。

因此,对于单链表实现获取第i个元素的数据的操作GetElem,在算法上相对要麻烦一些,大家不妨先思考一下。

获得链表第i个数据的算法思路:

声明一个结点p指向链表第一个结点,初始化j从1开始;当j<i时,就遍历链表,让p的指针向后移动,不断指向一下结点,j+1;若到链表末尾p为空,则说明第i个元素不存在;否则查找成功,返回结点p的数据。

有了以上的思路提示,试着写出代码!
再来给你点提示:

说白了,就是从头开始找,直到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历。而i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)。由于单链表的结构中没有定义表长,所以不能实现知道要循环多少次,因此也就不方便使用for来控制循环。其核心思想叫做“工作指针后移”,这其实也是很多算法的常用技术。

代码:

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */Status GetElem( LinkList L, int i, ElemType *e )
{int j;// 声明指针pLinkList p;// 指向链表p = L->next;// 初始化j = 1;// 不为空,且找到while( p && j<i ){// 赋值p = p->next;++j;}// 指向为假,或者j超过链表,任意符合条件结束if( !p || j>i ){return ERROR;}// 排除上面两个判断,找到最终结果*e = p->data;return OK;
}

第十一讲 线性表6

1.单链表的插入
假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系的变化。

大家参考下图思考一下:

思考后发觉根本用不着惊动其他结点!

只需要让s->next和p->next的指针做一点改变。

s->next = p->next;p->next = s;

我们通过图片来解读一下这两句代码。

我们考虑一下大部分初学者最容易搞坏脑子的问题:

这两句代码的顺序可不可以交换过来?先p->next = s;
再s->next = p->next;

大家发现没有?

如果先执行p->next的话会先被覆盖为s的地址那么s->next = p->next其实就等于s->next = s了。

所以这两句是无论如何不能弄反的,这点初学者一定要注意咯~

2.单链表第i个数据插入结点的算法思路:

声明一结点p指向链表头结点,初始化j从1开始;当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;若到链表末尾p为空,则说明第i个元素不存在;否则查找成功,在系统中生成一个空结点s;将数据元素e赋值给s->data;单链表的插入刚才两个标准语句;返回成功。

C语言代码实现:

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */Status ListInsert(LinkList *L, int i, ElemType e)
{int j;LinkList p, s;p = *L;j = 1;while( p && j<i )   // 用于寻找第i个结点{p = p->next;j++;}if( !p || j>i ){return ERROR;}s = (LinkList)malloc(sizeof(Node));s->data = e;s->next = p->next;p->next = s;return OK;
}

3.单链表的删除

假设元素a2的结点为q,要实现结点q删除单链表的操作。
其实就是将它的前继结点的指针绕过指向后继结点即可。

那我们所要做的,实际上就是一步:

可以这样:p->next = p->next->next;
也可以是:q=p->next; p->next=q->next;

那么我给大家提供算法的思路,由大家来写一下代码吧~

声明结点p指向链表第一个结点,初始化j=1;当j<1时,就遍历链表,让P的指针向后移动,不断指向下一个结点,j累加1;若到链表末尾p为空,则说明第i个元素不存在;否则查找成功,将欲删除结点p->next赋值给q;单链表的删除标准语句p->next = q->next;将q结点中的数据赋值给e,作为返回;释放q结点。

C语言代码实现:

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */Status ListDelete(LinkList *L, int i, ElemType *e)
{int j;LinkList p, q;p = *L;j = 1;while( p->next && j<i ){p = p->next;++j;}if( !(p->next) || j>i ){return ERROR;}q = p->next;p->next = q->next;*e = q->data;free(q);return OK;
}

4.效率PK
我们发现无论是单链表插入还是删除算法,它们其实都是由两个部分组成:

第一部分就是遍历查找第i个元素第二部分就是实现插入和删除元素。

从整个算法来说,我们很容易可以推出它们的时间复杂度都是O(n)。

再详细点分析:

如果在我们不知道第i个元素的指针位置单链表数据结构在插入和删除操作上与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第i个位置开始,插入连续10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个位置,所以每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显啦~

第十二讲 线性表7

1.单链表的整表创建
对于顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解。

而单链表和顺序存储结构就不一样了!

它不像顺序存储结构数据这么集中,它的数据可以是分散在内存各个角落的,他的增长也是动态的。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。

人生就要追求向单链表一样,灵活应变!

创建单链表的过程是一个动态生成链表的过程:

从“空表”的初始状态起,依次建立各元素结点并逐个插入链表。

所以单链表整表创建的算法思路如下:

声明一结点p和计数器变量i;初始化一空链表L;让L的头结点的指针指向NULL,即建立一个带头结点的单链表;循环实现后继结点的赋值和插入。

2.头插法建立单链表
头插法从一个空表开始,生成新结点,读取数据存放到新结点的数据域中。

然后将新结点插入到当前链表的表头上,直到结束为止。

简单来说,就是把新加进的元素放在表头后的第一个位置:

先让新节点的next指向头节点之后然后让表头的next指向新节点

嗯,用现实环境模拟的话就是插队的方法,始终让新结点插在第一的位置。

动画演示:

代码:

/* 头插法建立单链表示例 */void CreateListHead(LinkList *L, int n)
{LinkList p;// 注意:linkList 声明有*,后续调用就有*,p22所示//     LinkList 声明没有*,后续就没有,p18所示int i;// 长和time结合,生成更加像随机数的数srand(time(0));   // 初始化随机数种子*L = (LinkList)malloc(sizeof(Node));(*L)->next = NULL;for( i=0; i < n; i++ ){p = (LinkList)malloc(sizeof(Node));  // 生成新结点// 得到两位数,就和100取余p->data = rand()%100+1;p->next = (*L)->next;// *号优先级底,所以用括号包起来(*L)->next = p;}
}

3.尾插法建立单链表

头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。

就像现实社会我们鄙视:
插队不遵守纪律的孩子,那编程中我们也可以不这么干!

我们可以把思维逆过来:

把新结点都插入到最后,这种算法称之为尾插法。

动画演示:

代码实现:

/* 尾插法建立单链表演示 */void CreateListTail(LinkList *L, int n)
{LinkList p, r;int i;srand(time(0));*L = (LinkList)malloc(sizeof(Node));r = *L;for( i=0; i < n; i++ ){p = (Node *)malloc(sizeof(Node));p->data = rand()%100+1;// 备注:重点解释。// p可以理解为中介// r指向末尾(约定),ai-1 的next(r->next)指向ai,即pr->next = p;// r变到ai位置r = p;               }r->next = NULL;
}

第十三讲 线性表8 | 【小结】

1.单链表的整表删除

当我们不打算使用这个单链表时,我们需要把它销毁(真狠,不要就给别人嘛,还销毁~)。

其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。

单链表整表删除的算法思路如下:

声明结点p和q;将第一个结点赋值给p,下一结点赋值给q;循环执行释放p和将q赋值给p的操作;

代码展示:

Status ClearList(LinkList *L)
{LinkList p, q;p = (*L)->next;while(p){q = p->next;free(p);p = q;}(*L)->next = NULL;return OK;
}

这段算法代码里,常见的错误就是:

有同学会觉得q变量没有存在的必要。只需要在循环体内直接写free(p); p = p->next; 即可?

可这个世上没有无缘无故的爱,这样做会带来什么问题呢?

要知道p是一个结点,它除了有数据域,还有指针域。

当我们做free( p);时候,其实是对它整个结点进行删除和内存释放的工作。

而我们整表删除是需要一个个结点删除的,所以我们就需要q来记载p的下一个结点。

2.单链表结构与顺序存储结构优缺点

我们分别从存储分配方式、时间性能、空间性能三方面来做对比。

★存储分配方式:

顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。

★时间性能:

查找:
顺序存储结构O(1)。
单链表O(n)。插入和删除:
顺序存储结构需要平均移动表长一半的元素,时间为O(n)。
单链表在计算出某位置的指针后,插入和删除时间仅为O(1)。

★空间性能:

顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出。单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。

★综合分析:

若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不太合适了,单链表结构就可以大展拳脚了。当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。

★总之:

线性表的顺序存储结构和单链表结构各有其优缺点,不能简单的说哪个好,哪个不好,需要根据实际情况,来综合平衡采用哪种数据结构更能满足和达到需求和性能。

3.动画回顾

①头插法单链表

②尾插法建表

③单链表节点的插入

④单链表节点的删除

⑤顺序表的插入

⑥顺序表的删除运算

第十四讲 线性表9

1.静态链表

这一节课,我们试图通过静态链表的讲解来瞻仰古人的伟大!
(似乎人总要挂了之后才能变得伟大_

神马是静态链表呢?

又跟古人能有半毛钱关系?

地球人都知道C语言是个伟大的语言,他的魅力在于指针的灵活性。

使得它可以非常容易地操作内存中的地址和数据,这比其他高级语言更加灵活方便。(面向对象使用对象引用机制间接地实现了指针的某些功能)

但是古人还木有C语言丫,木有JAVA丫,只有原始的Basic,Fortran等早期的编程语言。

这些语言没有类似于C的指针功能,但是他们又想描述单链表,就没法实现了,怎么办呢?

真是不得不佩服他们的智慧,有人想出了用数组代替指针来描述单链表。

大家能猜一猜他们是如何做到的么?

在讲解原理之前,先让大家知道这种用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。

线性表的静态链表存储结构:

#define MAXSIZE 1000
typedef struct
{
ElemType data;  // 数据
int cur;        // 游标(Cursor)
} Component, StaticLinkList[MAXSIZE];

对静态链表进行初始化相当于初始化数组:

Status InitList(StaticLinkList space)
{
int i;
for( i=0; i < MAXSIZE-1; i++ )
space[i].cur = i + 1;space[MAXSIZE-1].cur = 0;return OK;
}

小甲鱼备忘录:

我们对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据。我们通常把未使用的数组元素称为备用链表。数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标。数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用。

2.静态链表的插入操作

有些喜欢思考的鱼油会说,你这不是挂羊头买鱼肉吗?

还是数组丫,貌似没看出太多单链表的端倪_

那我们接着从行为学的角度来剖析下,静态链表如何模拟单链表进行插入和删除的操作呢?

现在我们来看看:
如何实现元素的插入。

静态链表中要解决的是:

如何用静态模拟动态链表结构的存储空间分配,也就是需要的时候申请,不需要的时候释放。

我们前面说过,在动态链表中,结点的申请和释放分别借用C语言的malloc()和free()两个函数来实现。

在静态链表中:

操作的是数组,不存在像动态链表的结点申请和释放的问题。

所以我们需要自己实现这两个函数,才可以做到插入和删除操作。

为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的用游标链成一个备用链表,应该如何操作呢?

第十五讲 线性表10

1.静态链表的插入操作

上节课我们说到:(★ 第十四讲 线性表9 ★)

为了辨明数组中哪些分量未被使用,解决的方法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表。

每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

可能这样说大家都还是难以理解吧?

我们结合图片来讲解,这里我们假设要在A后边插入B:

因为B自古以来都是“2B”,A指向了2B(下标5)

看代码:

/* 在静态链表L中第i个元素之前插入新的数据元素e */Status ListInsert( StaticLinkList L, int i, ElemType e )
{int j, k, l;k = MAX_SIZE - 1;    // 数组的最后一个元素if( i<1 || i>ListLength(L)+1 ){return ERROR;}// 获取空闲的第一个分量的下标j = Malloc_SLL(L);if( j ){L[j].data = e;// 1是新插入的元素,即B// 插入到第二个元素之前,所以是i-1// i=2,循环执行1次for( l=1; l <= i-1; l++ ){// L[k],l[最后一个元素],游标指向第一个元素,赋值给Kk = L[k].cur;}// 即2取出来,存放到L[j]L[j].cur = L[k].cur;// j5,存放到L[k]L[k].cur = j;return OK;}return ERROR;// 获得空闲分量的下标:
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;
if( space[0].cur )
space[0].cur = space[i].cur;// 把它的下一个分量用来作为备用。
return i;
}}

第十六讲 线性表11 | 【实例改革】

1.静态链表的删除操作

我们的故事还没结束,小C看到小A和2B这样非法的勾当。

内心觉得很不爽,一句话也不说就离开了队伍。。。。。。

具体过程请看代码:

/* 删除在L中的第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{int j, k;if( i<1 || i>ListLength(L) ){return ERROR;}k = MAX_SIZE - 1;for( j=1; j <= i-1; j++ ){k = L[k].cur;    // k1 = 1, k2 = 5}j = L[k].cur;        // j = 2L[k].cur = L[j].cur;Free_SLL(L, j);return OK;
}/* 将下标为k的空闲结点回收到备用链表 */
void Free_SLL(StaticLinkList space, int k)
{space[k].cur = space[0].cur;space[0].cur = k;
}/* 返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{int j = 0;int i = L[MAXSIZE-1].cur;while(i){i = L[i].cur;j++;}return j;
}

2.静态链表优缺点总结

优点:

在插入和删除操作时,只需要修改游标,不需要移动元素。从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。

缺点:

没有解决连续存储分配(数组)带来的表长难以确定的问题。失去了顺序存储结构随机存取的特性。

总结:

总的来说,静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法。尽管我们可以用单链表就不用静态链表了,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。

3.结腾讯面试题
题目:

快速找到未知长度单链表的中间节点。

既然是面试题就一定有普通方法和高级方法。
而高级方法无疑会让面试官大大加分!

普通的方法很简单:

首先遍历一遍单链表以确定单链表的长度L。然后再次从头节点出发循环L/2次找到单链表的中间节点。算法复杂度为:O(L+L/2)=O(3L/2)。

代码:

#include "stdio.h"#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0typedef int Status;   /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */typedef struct Node
{ElemType data;struct Node *next;
}Node;typedef struct Node *LinkList; /* 定义LinkList */Status visit(ElemType c)
{printf("%d ",c);return OK;
}/* 初始化顺序线性表 */
Status InitList(LinkList *L)
{*L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */if(!(*L)) /* 存储分配失败 */{return ERROR;}(*L)->next=NULL; /* 指针域为空 */return OK;
}/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(LinkList L)
{int i=0;LinkList p=L->next; /* p指向第一个结点 */while(p){i++;p=p->next;}return i;
}/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{LinkList p=L->next;while(p){visit(p->data);p = p->next;}printf("\n");return OK;
}/*  随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{LinkList p,r;int i;srand(time(0));                      /* 初始化随机数种子 */*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */r=*L;                                /* r为指向尾部的结点 */for (i=0; i < n; i++){p = (Node *)malloc(sizeof(Node)); /*  生成新结点 */p->data = rand()%100+1;           /*  随机生成100以内的数字 */r->next=p;                        /* 将表尾终端结点的指针指向新结点 */r = p;                            /* 将当前的新结点定义为表尾终端结点 */}r->next = NULL;                       /* 表示当前链表结束 */// 创建有环链表//r->next = p;
}Status GetMidNode(LinkList L, ElemType *e)
{LinkList search, mid;mid = search = L;while (search->next != NULL){//search移动的速度是 mid 的2倍if (search->next->next != NULL){search = search->next->next;mid = mid->next;}else{search = search->next;}}*e = mid->data;return OK;
}int main()
{LinkList L;Status i;char opp;ElemType e;int find;int tmp;i=InitList(&L);printf("初始化L后:ListLength(L)=%d\n",ListLength(L));printf("\n1.查看链表 \n2.创建链表(尾插法) \n3.链表长度 \n4.中间结点值 \n0.退出 \n请选择你的操作:\n");while(opp != '0'){scanf("%c",&opp);switch(opp){case '1':ListTraverse(L);printf("\n");break;case '2':CreateListTail(&L,20);printf("整体创建L的元素(尾插法):\n");ListTraverse(L);printf("\n");break;case '3'://clearList(pHead);   //清空链表printf("ListLength(L)=%d \n",ListLength(L));printf("\n");break;case '4'://GetNthNodeFromBack(L,find,&e);GetMidNode(L, &e);printf("链表中间结点的值为:%d\n", e);//ListTraverse(L);printf("\n");break;case '0':exit(0);}}
}

这也太长了。。。

能否再优化一下这个时间复杂度呢?

有一个很巧妙的方法:

利用快慢指针!

高薪玩法

利用快慢指针原理:

设置两个指针*search、*mid都指向单链表的头节点。其中* search的移动速度是*mid的2倍。当*search指向末尾节点的时候,mid正好就在中间了。这也是标尺的思想。

代码实现:

Status GetMidNode(LinkList L, ElemType *e)
{LinkList search, mid;mid = search = L;while (search->next != NULL){//search移动的速度是 mid 的2倍if (search->next->next != NULL){search = search->next->next;mid = mid->next;}else{search = search->next;}}*e = mid->data;return OK;
}

课后作业:

写一个完整的程序,实现随机生成20个元素的链表(尾插法或头插法任意),用我们刚才学到的方法快速查找中间结点的值并显示。

第十七讲 线性表12

1.循环链表

打个比方:

就是从前山上有座庙,庙里有个老和尚和一个小和尚,有一天老和尚对小和尚说“从前山上有座庙,庙里有个老和尚和一个小和尚,有一天老和尚对小和尚说“从前 。。。。。。

对于单链表,由于每个结点只存储了向后的指针,到了尾部标识就停止了向后链的操作。
也就是说,按照这样的方式,只能索引后继结点不能索引前驱结点。这会带来什么问题呢?

例如不从头结点出发,就无法访问到全部结点。
事实上要解决这个问题也并不麻烦,只需要:

将单链表中终端结点的指针端由空指针改为指向头结点,问题就结了。

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环。

这种头尾相接的单链表成为单循环链表,简称循环链表。

(1)单循环链表

这里并不是说:

循环链表一定要有头结点。

其实循环链表的单链表的主要差异在于“循环的判断空链表的条件上”

原来判断head->next是否为null,现在则是head->next是否等于head。

回到刚才的问题,由于终端结点用尾指针rear指示。

则查找终端结点是O(1),而开始结点是rear->next->next,当然也是O(1)。

代码讲解

①初始化

/*初始化循环链表*/
void ds_init(node **pNode)
{int item;node *temp;node *target;printf("输入结点的值,输入0完成初始化\n");while(1){scanf("%d", &item);fflush(stdin);if(item == 0)return;if((*pNode) == NULL){ /*循环链表中只有一个结点*/*pNode = (node*)malloc(sizeof(struct CLinkList));if(!(*pNode))exit(0);(*pNode)->data = item;(*pNode)->next = *pNode;}else{/*找到next指向第一个结点的结点*/for(target = (*pNode); target->next != (*pNode); target = target->next);/*生成一个新的结点*/temp = (node *)malloc(sizeof(struct CLinkList));if(!temp)exit(0);temp->data = item;temp->next = *pNode;target->next = temp;}}
}

②插入

/*链表存储结构的定义*/
typedef struct CLinkList
{int data;struct CLinkList *next;
}node;/*插入结点*/
/*参数:链表的第一个结点,插入的位置*/
void ds_insert(node **pNode , int i)
{node *temp;node *target;node *p;int item;int j = 1;printf("输入要插入结点的值:");scanf("%d", &item);if(i == 1){ //新插入的结点作为第一个结点temp = (node *)malloc(sizeof(struct CLinkList));if(!temp)exit(0);temp->data = item;/*寻找到最后一个结点*/for(target = (*pNode); target->next != (*pNode); target = target->next);temp->next = (*pNode);target->next = temp;*pNode = temp;}else{target = *pNode;for( ; j < (i-1); ++j ){target = target->next;}  // target指向第三个元素的temp = (node *)malloc(sizeof(struct CLinkList));if(!temp)exit(0);temp->data = item;p = target->next;target->next = temp;temp->next = p;}
}

③删除

/*删除结点*/
void ds_delete(node **pNode, int i)
{node *target;node *temp;int j = 1;if(i == 1){ //删除的是第一个结点/*找到最后一个结点*/for(target = *pNode; target->next != *pNode;target = target->next);temp = *pNode;*pNode = (*pNode)->next;target->next = *pNode;free(temp);}else{target = *pNode;for( ; j < i-1; ++j){target = target->next;}temp = target->next;target->next = temp->next;free(temp);}
}

④返回结点所在位置

/*返回结点所在位置*/
int ds_search(node *pNode, int elem)
{node *target;int i = 1;for(target = pNode; target->data != elem && target->next != pNode; ++i){target = target->next;}if(target->next == pNode) /*表中不存在该元素*/return 0;elsereturn i;
}

第十八讲 线性表13 | 【约瑟夫(花样自杀)问题】

1.约瑟夫问题

据说著名犹太历史学家 Josephus有过以下的故事:

在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式。41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。最后剩的这两个人只要串通好,其它人都挂了,他们就可以走了哈~

激烈讨论

热心鱼油:

你TMD在说啥*&(*%@……!

小甲鱼:

我的意思是约瑟夫问题跟我们讲的循环链表有啥关系?

鱼油:

它们都是圆!

小甲鱼:

真聪明,亲一个^_^

小甲鱼:

对的,约瑟夫问题里边41个人是围成一个圆圈,我们的循环链表也是一个圆圈,所以可以模拟并让计算机运行告诉我们结果!

理论实践

代码:

//n个人围圈报数,报m出列,最后剩下的是几号?
#include <stdio.h>
#include <stdlib.h>typedef struct node
{int data;struct node *next;
}node;node *create(int n)
{node *p = NULL, *head;head = (node*)malloc(sizeof (node ));p = head;node *s;int i = 1;if( 0 != n ){while( i <= n ){s = (node *)malloc(sizeof (node));s->data = i++;    // 为循环链表初始化,第一个结点为1,第二个结点为2。p->next = s;p = s;}s->next = head->next;}free(head);return s->next ;
}int main()
{int n = 41;int m = 3;int i;node *p = create(n);node *temp;m %= n;   // m在这里是等于2while (p != p->next ){for (i = 1; i < m-1; i++){p = p->next ;}printf("%d->", p->next->data );temp = p->next ;                                //删除第m个节点p->next = temp->next ;free(temp);p = p->next ;}printf("%d\n", p->data );return 0;
}

第十九讲 线性表14

1.循环链表的特点

回顾一下:

在单链表中,我们有了头结点时,我们可以用O(1)的时间访问第一个结点,但对于要访问最后一个结点,我们必须要挨个向下索引,所以需要O(n)的时间。

大家猜的没错,如果用上今天我们学习到的循环链表的特点,用O(1)的时间就可以由链表指针访问到最后一个结点。

不过我们需要改造一下现有的循环链表,我们不用头指针。

而是用指向终端结点的尾指针来表示循环链表。
此时查找开始结点和终端结点都很方便了。

那么按照这个逻辑的话:

判断是否为空链表的条件应该如何呢?

就是:

判断rear是否等于rear->next,大家猜对了吗?

循环链表的特点是无须增加存储量,仅对链接方式稍作改变,即可使得表处理更加方便灵活。

2.例题

题目:

实现将两个线性表(a1,a2,…,an)和(b1,b2,…,bm)连接成一个线性表(a1,…,an,b1,…bm)的运算。

分析:

若在单链表或头指针表示的单循环表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)。若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间是O(1)。


分为4个主要步骤,请见代码:

//假设A,B为非空循环链表的尾指针
LinkList Connect(LinkList A,LinkList B)
{       LinkList p = A->next;                //1 保存A表的头结点位置A->next = B->next->next;        //2 B表的开始结点链接到A表尾free(B->next);        //3 释放B表的头结点,初学者容易忘记B->next = p;               return B;                //4 返回新循环链表的尾指针
}

3.判断链表是否有环

有环的定义:

链表的尾节点指向了链表中的某个节点。


那么要判断单链表中是否有环,主要有以下两种方法:

方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。如图,当p从6走到3时,用了6步,此时若q从head出发,则只需两步就到3,因而步数不等,出现矛盾,存在环。方法二:使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环

代码实现:

#include "stdio.h"#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */typedef struct Node
{ElemType data;struct Node *next;
}Node, *LinkList;/* 初始化带头结点的空链表 */
Status InitList(LinkList *L)
{*L = (LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */if(!(*L)) /* 存储分配失败 */return ERROR;(*L)->next=NULL; /* 指针域为空 */return OK;
}/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(LinkList L)
{int i=0;LinkList p=L->next; /* p指向第一个结点 */while(p){i++;p=p->next;}return i;
}/*  随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList *L, int n)
{LinkList p;int i;srand(time(0));                         /*  初始化随机数种子 */*L = (LinkList)malloc(sizeof(Node));(*L)->next = NULL;                      /*  建立一个带头结点的单链表 */for (i=0; i < n; i++){p = (LinkList)malloc(sizeof(Node)); /*  生成新结点 */p->data = rand()%100+1;             /*  随机生成100以内的数字 */p->next = (*L)->next;(*L)->next = p;                                                /*  插入到表头 */}
}/*  随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{LinkList p,r;int i;srand(time(0));                      /* 初始化随机数种子 */*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */r = *L;                              /* r为指向尾部的结点 */for (i=0; i < n; i++){p = (Node *)malloc(sizeof(Node)); /*  生成新结点 */p->data = rand()%100+1;           /*  随机生成100以内的数字 */r->next=p;                        /* 将表尾终端结点的指针指向新结点 */r = p;                            /* 将当前的新结点定义为表尾终端结点 */}r->next = (*L)->next->next;
}// 比较步数的方法
int HasLoop1(LinkList L)
{LinkList cur1 = L;  // 定义结点 cur1int pos1 = 0;       // cur1 的步数while(cur1){                       // cur1 结点存在LinkList cur2 = L;  // 定义结点 cur2int pos2 = 0;       // cur2 的步数while(cur2){                           // cur2 结点不为空if(cur2 == cur1){                       // 当cur1与cur2到达相同结点时if(pos1 == pos2)    // 走过的步数一样break;          // 说明没有环else                // 否则{printf("环的位置在第%d个结点处。\n\n", pos2);return 1;       // 有环并返回1}}cur2 = cur2->next;      // 如果没发现环,继续下一个结点pos2++;                 // cur2 步数自增}cur1 = cur1->next;  // cur1继续向后一个结点pos1++;             // cur1 步数自增}return 0;
}// 利用快慢指针的方法
int HasLoop2(LinkList L)
{int step1 = 1;int step2 = 2;LinkList p = L;LinkList q = L;while (p != NULL && q != NULL && q->next != NULL){p = p->next;if (q->next != NULL)q = q->next->next;printf("p:%d, q:%d \n", p->data, q->data);if (p == q)return 1;}return 0;
}int main()
{LinkList L;Status i;char opp;ElemType e;int find;int tmp;i = InitList(&L);printf("初始化L后:ListLength(L)=%d\n",ListLength(L));printf("\n1.创建有环链表(尾插法) \n2.创建无环链表(头插法) \n3.判断链表是否有环 \n0.退出 \n\n请选择你的操作:\n");while(opp != '0'){scanf("%c",&opp);switch(opp){case '1':CreateListTail(&L, 10);printf("成功创建有环L(尾插法)\n");printf("\n");break;case '2':CreateListHead(&L, 10);printf("成功创建无环L(头插法)\n");printf("\n");break;case '3':printf("方法一: \n\n");if( HasLoop1(L) ){printf("结论:链表有环\n\n\n");}else{printf("结论:链表无环\n\n\n");}printf("方法二:\n\n");if( HasLoop2(L) ){printf("结论:链表有环\n\n\n");}else{printf("结论:链表无环\n\n\n");}printf("\n");break;case '0':exit(0);}}}

第二十讲 线性表15 | 【魔术师发牌问题&重要说明】

1.魔术师发牌问题

问题提炼:

牌的开始顺序是如何安排的?

友情提醒:

自己拿一副扑克牌,实际模拟下!

代码实现:

#include <stdio.h>
#include <stdlib.h>#define  CardNumber 13typedef struct node
{int data;struct node *next;
}sqlist, *linklist;linklist CreateLinkList()
{linklist head = NULL;linklist s, r;int i;r = head;for(i=1; i <= CardNumber; i++){s = (linklist)malloc(sizeof(sqlist));s->data = 0;if(head == NULL)head = s;elser->next = s;r = s;}r->next = head;return head;
}// 发牌顺序计算
void Magician(linklist head)
{linklist p;int j;int Countnumber = 2;p = head;p->data = 1;  //第一张牌放1while(1){for(j=0; j < Countnumber; j++){p = p->next;if(p->data != 0)  //该位置有牌的话,则下一个位置{p->next;j--;}}if(p->data == 0){p->data = Countnumber;Countnumber ++;if(Countnumber == 14)break;}}
}// 销毁工作
void DestoryList(linklist* list)
j
}int main()
{linklist p;int i;p = CreateLinkList();Magician(p);printf("按如下顺序排列:\n");for (i=0; i < CardNumber; i++){printf("黑桃%d ", p->data);p = p->next;}DestoryList(&p);return 0;
}

2.拉丁方阵问题


背景说明:

著名数学家和物理学家欧拉使用拉丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名。拉丁方阵是一种n×n的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,并且每种元素在一行和一列中恰好出现一次。

例如下图是一个3×3的拉丁方阵:


请利用循环链表实现一个拉丁方阵!

代码:

#include<stdio.h>
#define N 6 /*确定N值*/
int main()
{
int i,j,k,t;
printf("The possble Latin Squares of order %d are:\n",N);
for(j=0;j<N;j++) /*构造N个不同的拉丁方阵*/
{
for(i=0;i<N;i++)
{
t=(i+j)%N; /*确定该拉丁方阵第i 行的第一个元素的值*/
for(k=0;k<N;k++) /*按照环的形式输出该行中的各个元素*/
printf("%d",(k+t)%N+1);
printf("\n");
}
printf("\n");
}
}

第二十一讲 线性表16

1.双向链表

这货我们地球人把他称为火车(798网红火车)。

有了它:全世界每年春季最大规模的物种迁移才会发生在中国!

大家都知道,任何事物出现的初期都显得有些不完善。

例如我们的火车刚发明的时候是只有一个“头”的,所以如果它走的线路是如下:

A->B->C->D->E->F->G->H->I->J->K->L->A

假设这会儿火车正停在K处呢,要他送一批货到J处,那么它将走的路线是:

K->L->A->B->C->D->E->F->G->H->I->J

嗯,所以后来:
我们的火车就都有两个头了。

看完这个例子,大家就明白双向链表的必要性了吧。

双向链表节点结构

代码演示:

typedef struct DualNode
{
ElemType data;
struct DualNode *prior;  //前驱结点
struct DualNode *next;   //后继结点
} DualNode, *DuLinkList;


既然单链表可以有循环链表,那么双向链表当然也可以有。

问大家一个问题:

由于这是双向链表,那么对于链表中的某一个结点p,它的后继结点的前驱结点是什么?

2.插入操作

插入操作其实并不复杂,不过顺序很重要,千万不能写反了

代码实现:

s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;

关键在于交换的过程中不要出现矛盾,例如第四步先被执行了。

那么p->prior就会提前变成s,使得插入的工作出错。

3.删除操作

如果上面的插入操作理解了,那么再来理解接下来的删除操作就容易多了。

代码实现:

p->prior->next = p->next;
p->next->prior = p->prior;
free(p);

最后总结一下:

双向链表相对于单链表来说,是要更复杂一点。每个结点多了一个prior指针,对于插入和删除操作的顺序大家要格外小心。不过,双向链表可以有效提高算法的时间性能,说白了就是用空间来换取时间。

第二十二讲 线性表17 | 【类Caser(凯撒)密码实践课】

课堂演示题目:

要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:
DEFGHIJKLMNOPQRSTUVWXYZABC同时需要支持负数,例如用户输入-3,输出结果:
XYZABCDEFGHIJKLMNOPQRSTUVW

本节课为小甲鱼老师手敲代码课

视频中,展现了高超的程序操作技巧,只可意会!

核心思想:

利用ASCII码,结合双向循环链表循环完成结果对应变化。

最终代码实现:

#include<stdio.h>
#include<stdlib.h>#define OK 1
#define ERROR 0;typedef char ElemType;
typedef int Status;
typedef struct DualNode
{ElemType data;struct DualNode *prior;struct DualNode *next;
}DualNode,*DuLinkList;Status InitList(DuLinkList *L)
{DualNode *p,*q;int i;*L=(DuLinkList)malloc(sizeof(DualNode));if(!(*L)){return ERROR;}(*L)->next=(*L)->prior=NULL;p=(*L);for(i=0;i<26;i++){q=(DualNode *)malloc(sizeof(DualNode));if(!q){return ERROR;}q->data='A'+i;q->prior=p;q->next=p->next;p->next=q;p=q;}p->next=(*L)->next;(*L)->next->prior=p;return OK;
}void caser(DuLinkList *L,int i)
{if(i>0){do{(*L)=(*L)->next;}while(--i);}if(i<0){i=i-1;(*L)=(*L)->next;do{(*L)=(*L)->prior;}while(++i);}}int main()
{DuLinkList L;int i,n;InitList(&L);printf("请输入一个整数:\n");scanf("%d",&n);printf("\n");caser(&L,n);for(i=0;i<26;i++){L=L->next;printf("%c",L->data);}printf("\n");return 0;
}

成成你好呀笔记整理(知识点合集一)相关推荐

  1. JVM面试知识点合集 — Android 春招 2022

    JVM面试知识点合集 - Android 春招 2022 星光不问赶路人,时间不负有心人 Tips:文章较长,可以在侧栏目点击子标题,快速跳转 喜欢的话,就一键三连吧

  2. 全国计算机二级office选择题知识点,全国计算机二级考试MS office选择题知识点合集(通用)...

    全国计算机二级考试MS office选择题知识点合集(通用) 计算机二级选择题考点总结 2014年计算机二级 office高级应用考试基础知识 计算机的发展.类型及其应用领域. 1.计算机 (comp ...

  3. Python入门语法知识点合集

    老袁今天主要是给新手朋友们,分享一下语法知识点的一些重要内容合集,由于内容多,文字抒写的话可鞥很多人看不懂,所以花费了点时间做了图片给大家看和学习,喜欢的可以收藏起来,或者说不清晰的可以联系我,我私发 ...

  4. 2019年7月抖音热门音乐整理精选合集- 免费下载

    抖音最新7月份整理的超好听音乐合集 小编已打包 分享给共同爱好的朋友们 有需要的快去下载吧, 很担心会被和谐 熬夜整理,很多音乐都是我自费购买的无损音乐 就为了给大家一个完美的音质. 下载地址 链接: ...

  5. 成成你好呀笔记整理(知识点合集二)

    第二十三讲 栈和队列 1.栈的定义 这把M1911A1深藏"栈"的功与名! 栈是一种重要的线性结构,可以这样讲,栈是前面讲过的线性表的一种具体形式. 就像我们刚才的例子,栈这种后进 ...

  6. 成成你好呀笔记整理(知识点合集六)

    第六十二讲 最小生成树|[普里姆算法] 1.最小生成树 小苍童鞋的难题: 领导要求在A-I的9个网络中添加通信网络,连线之间的值叫做权值,表示村庄之间搭设通信网络的成本,领导要求用最小的成本完成任务. ...

  7. 前端学习笔记01---HTML5、CSS3、移动端前端基础学习知识点合集

    文章目录 HTML结构 一.标签(标记.元素) 二.列表,表格,表单 1.列表 2.图片 3.超链接--实现不同页面的跳转 4.table表格 5.表单1 --收集用户信息给后端input CSS表现 ...

  8. 统计学习方法第二版知识点合集 - 自用笔记

    第一篇 监督学习 第一章 统计学习及监督学习概论 1.1 统计学习 统计学习是关于计算机基于数据构建概率统计模型并运用模型对数据进行预测与分析的一门学科,也称统计机器学习,也就是我们俗称的机器学习. ...

  9. Java 阶段面试 知识点合集 - 我们到底能走多远系列(15)

    我们到底能走多远系列(15) 扯淡:这些知识点来源是通过面试涉及到的,面的公司不多,知识点涉及也不多,我每次面试后都在备忘录里写下有用的东西,集合起来分享一下,因为是知识点,所以就不出什么面试题目啦. ...

最新文章

  1. python多久能上手_小白学习Python,怎样能够快速入门上手
  2. sqlserver之定位死锁(经验分享)
  3. mybatis log4j打印sql
  4. count数据库优化oracle,迷惑性SQL性能问题排查与优化
  5. linux下/var/run目录下.pid文件的作用(文件锁,防止重复启动)
  6. 洛谷P1856 [USACO5.5]矩形周长Picture
  7. 练习:写一个脚本,完成以下任务
  8. Windows 7丢失用户、密码解决办法-我体验了!
  9. c make干什么用得_CMake简单使用
  10. 蓝桥杯Java组常用算法与技巧
  11. 【Windows 问题系列第 13 篇】如何修改 Windows10 的鼠标指针样式
  12. 大学课程 | 《计算机系统结构》详细知识点总结
  13. 可调稳压电源lm317实验报告_LM317可调稳压电源实训实验.doc
  14. 1招提升Java单元测试ROI
  15. input隐藏变显示
  16. html图片幻灯片效果,使用CSS3实现的超酷幻灯图片效果
  17. html5理财计算,理财收益怎么算(一般理财产品的收益计算方法)
  18. 网站在多IE版本兼容性测试工具IETester的使用方法
  19. poj 3626 Mud Puddles
  20. 定理在数学中的简写形式_初中数学定义、定理汇总

热门文章

  1. 补充函数编程,程序功能要求:有一个已知数组d,程序调用函数out输出数组 d的所有元素;调用函数plus对数组d中所有大于0的元素分别加上30;调用函数 cpy将d数组所有元素
  2. Frameworks Brightness
  3. Mac上php环境配置
  4. nVidia的物理系统
  5. html处理文字换行
  6. 福州大学计算机学院李敏,福州大学厦门工艺美术学院
  7. The Shawshank Redemption-19
  8. 用Python绘制诱人的桑基图,一眼看透熬夜和狗粮的秘密...
  9. lib lib32 lib64等lib文件夹的说明
  10. 赵小楼《天道》《遥远的救世主》深度解析(61)丁元英和芮小丹的“着相”与“不着相”