线性表是具有n个相同数据类型元素的有限序列,除了第一个元素无直接前驱,最后一个元素无直接后继外,其余元素均有唯一的前驱和后继。元素之间具有一对一的关系。如图所示:
线性表在计算机中的存储结构分为两种,一种是顺序存储,一种是链式存储。顺序存储的线性表也被称为顺序表。它是指,顺序表中的所有元素存放在一块地址连续的空间中,相邻元素间的物理地址连续,以达到其在逻辑结构上相邻的目的。
首先,定义顺序表的存储结构:
1. 定义顺序表的存储表示
在C语言中,要表示n个元素在物理位置上的相邻结构,可以采用数组来表示。数组的最大长度必须提前定义,同时,还需要知道数组中实际存放的元素个数,这样,顺序表便可以表示出来了。为便于数组元素类型以及最大长度的改变,可以做如下宏定义和替换:
#define SeqListMaxSize 1000//顺序表中数组的最大元素个数
typedef char SeqListType;//顺序表中数组数据的类型
顺序表的结构定义如下:
//定义顺序表的结构体数组
7 typedef struct SeqList
8 {
9 SeqListType data[SeqListMaxSize];
10 int size;//数组的实际长度
11 }SeqList;//将结构体重命名为SeqList
然后,对已经定义的结构体进行初始化:
2. 初始化顺序表
顺序表中有两个成员,一个是数组的实际元素个数值,一个是数组,顺序表定义好后,里面并没有有效元素,所以将size初始化为0,而数组在定义后其元素均被赋予随机值,因为size为0,所以数组中并无有效元素,所以不用对其初始化。故,顺序表的初始化如下:
3 void SeqListInit(SeqList *seqlist)//初始化顺序表 ,顺序表为seqlist
4 {
5 if(seqlist == NULL)
6 {
7 //非法输入
8 return;
9 }
10
11 seqlist->size = 0;//将数组长度初始化为0
12 return;
13 }
下面,来实现顺序表的一些基本操作:
3. 尾插法建立顺序表
对顺序表插入元素时,每一次从尾部插入,如下图所示:
(1)因为是要数组中添加元素,如果数组实际元素个数已经达到数组所能承载的最大元素个数SeqListMaxSize,此时,便不能再往里插入元素。
(2)若数组没有满,因为顺序表成员size表示的是现有数组的元素个数,所以现有数组的最大下标为size-1,新尾插的元素value放在现有数组最后一个元素后面,即放在下标为size处,再使插入元素后的数组元素个数size加1即可。
//向顺序表seqlist中尾插数据value
void SeqListPushBack(SeqList *seqlist,SeqListType value)
17 {
18 //指针判空
19 if(seqlist == NULL)
20 {
21 //非法输入
22 return;
23 }
24
25 //顺序表判满
26 if(seqlist->size >= SeqListMaxSize)
27 {
28 //顺序表满
29 return;
30 }
31
32 seqlist->data[seqlist->size] = value;
33 seqlist->size++;
34 return;
35 }
4. 尾部删除元素
从尾部删除数据元素,是尾部插入数据的逆行进行,如下图所示:
(1)要在数组中删除元素,如果数组已空,即数组的元素个数size为0,则会删除失败。
(2)若数组不为空,只需将数组的元素个数减1,是原有数组的最后一个元素成为无效元素即可。
//在数据表seqlist中尾删数据value
void SeqListPopBack(SeqList *seqlist)
39 {
40 //指针判空
41 if(seqlist == NULL)
42 {
43 //非法输入
44 return;
45 }
46
47 //顺序表判空
48 if(seqlist->size == 0)
49 {
50 //顺序表为空
51 return;
52 }
53
54 seqlist->size--;
55 return;
56 }
5. 头插法建立数据表
每次插入的数据都位于元素数据的前面,如图所示:
(1)与尾插类似,插入元素时要判断数组是否已满
(2)若在头部插入一个元素,则原有元素要依次往后移,将数组下标为0的位置空出来,新元素在放置在下标为0的位置处。元素个数size加1即可,如下图:
未插入元素前,每个元素都要往后移,所以要一个个循环,设置计数器变量i初始为size,作为移动后最后一个元素的下标,再将下标为i-1的元素赋值给下标为i的元素,i每次减少1,下标i-1最小为0时,便可将原有数组元素全部移动完成,所以,需满足:
初始时:i=size
移动过程中:i-1>=0 => i>=1 => i>0
代码如下:
//向数据表seqlist中头插数据value
void SeqListPushFront(SeqList *seqlist,SeqListType value)
60 {
61 if(seqlist == NULL)
62 {
63 //非法输入
64 return;
65 }
66
67 if(seqlist->size >= SeqListMaxSize)
68 {
69 //顺序表满
70 return;
71 }
72
73 int i = seqlist->size;
74 for(i = seqlist->size;i > 0;i--)
75 {
76 seqlist->data[i] = seqlist->data[i-1];
77 }
78 seqlist->data[0] = value;
79 seqlist->size++;
80 return;
81 }
6. 顺序表头删元素
每次从头部删除一个元素,如下图所示:
(1)与尾删类型,先判断数组是否为空。为空,则删除失败。
(2)不为空,从下标为1的元素开始依次往前移,知道最后一个元素前移完成,数组元素个数减1,移动完成后,原有数组最后一个元素为无效元素。如下图:
定义计数器变量i,初始为0,将下标为i+1的数组元素赋值为下标为i的数组元素,i逐次加1,直到i+1最大为size-1为止,此时,所有数组元素均可前移,所以,需满足:
初始:i=0;
移动过程中:i+1<=size-1 => i<=size-2 => i<size-1
代码如下:
//从顺序表中seqlist头删数据
void SeqListPopFront(SeqList *seqlist)
85 {
86 if(seqlist == NULL)
87 {
88 //非法输入
89 return;
90 }
91
92 if(seqlist->size == 0)
93 {
94 //顺序表已空
95 return;
96 }
97
98 int i = 0;
99 for(i = 0;i <= (seqlist->size) - 2;i++)
100 {
101 seqlist->data[i] = seqlist->data[i+1];
102 }
103 seqlist->size--;
104 return;
105 }
7. 顺序表任意位置插入元素
(1)插入元素时要判断数组是否已满,已满,则插入失败。
(2)因为数组在物理位置上是连续存放的,未插入前数组的最大下标为size-1,所以,Pos的范围是:[0,size],如果Pos不在该范围内,则插入失败。
(3)若满足前两个条件,假设在下标为Pos处插入元素,此时,从下标为Pos开始(包括Pos),之后的元素均要依次后移,将下标为Pos的位置空余出来,将新插入的数据放置在该处。数组元素加1。
插入的位置下标可以是0到size中的任意一个,所以设置计数器变量i,初始为size,将下标为i-1的元素赋值给下标为i的元素,直到i-1最小为Pos为止,所以:
初始:i=size
移动过程中,i-1>=Pos => i>=Pos+1 => i>Pos
代码如下:
//在顺序表seqlist中任意位置插入元素value
//Pos是插入元素的位置下标
void SeqListInsert(SeqList *seqlist,int Pos,SeqListType value)
109 {
110 //指针判空
111 if(seqlist == NULL)
112 {
113 //非法输入
114 return;
115 }
116
117 //顺序表判满
118 if(seqlist->size == SeqListMaxSize)
119 {
120 //顺序表已满
121 return;
122 }
123
124 //判断插入位置是否合法
125 if(Pos < 0 || Pos > seqlist->size)
126 {
127 //插入位置不合法
128 return;
129 }
130
131 int i = seqlist->size;
132 for(;i > Pos;i--)
133 {
134 seqlist->data[i] = seqlist->data[i-1];
135 }
136 seqlist->data[Pos] = value;
137 seqlist->size++;
138 return;
139 }
8. 在任意位置删除元素(1)判断数组是否为空,为空则删除失败
(2)判断删除的位置下标Pos的合法性,数组已有的元素下标是[0,size-1],所以Pos也必须在该范围内,否则,删除失败。
(3)满足上述两个条件后,只需将Pos(不包括Pos)之后的所有元素依次往前移即可,然后数组的元素个数减1。前移完成后,原数组最后一个元素变为无效元素。
Pos的范围是[0,size-1],设置计数器变量i,初始为Pos,将下标为i的数组元素赋值给下标为i+1的数组元素,i逐次加1,直到i+1最大为size-1即可,所以:
初始时:i = pos;
移动过程中:i+1 <= size - 1 => i <= size - 2 => i< size - 1;
移动完成后:size = size - 1;
代码如下:
//在顺序表seqlist的任意位置删除元素
//Pos为要删除元素所在的下标
void SeqListErase(SeqList *seqlist,int Pos)
215 {
216 if(seqlist == NULL)
217 {
218 //非法输入
219 return;
220 }
221
222 if(seqlist->size == 0)
223 {
224 //顺序表为空
225 return;
226 }
227
228 if(Pos < 0 || Pos > seqlist->size - 1)
229 {
230 //删除位置不合法
231 return;
232 }
233
234 int i = Pos;
235 for(i = Pos;i < seqlist->size - 1;i++)
236 {
237 seqlist->data[i] = seqlist->data[i+1];
238 }
239 seqlist->size--;
240 return;
241 }
9. 在顺序表任意位置读取数据
(1)判断数组是否为空,为空则读取失败
(2)判断读取的位置下标Pos的合法性,数组已有的元素下标是[0,size-1],所以Pos也必须在该范围内,否则,读取失败。
(3)满足上述两个条件后,只需将下标为Pos的数组元素存放起来即可。
//在顺序表seqlist中任意位置读取数据
//Pos为读取元素的位置下标,读取的数据放入c中
void SeqListGet(SeqList *seqlist,int Pos,SeqListType *c)
141 {
142 if(seqlist == NULL)
143 {
144 //非法输入
145 return;
146 }
147
148 if(seqlist->size == 0)
149 {
150 //顺序表已空
151 return;
152 }
153
154 if(Pos < 0 || Pos > seqlist->size-1)
155 {
156 //读取位置不合法
157 return;
158 }
159 *c = seqlist->data[Pos];
160 return;
161 }
10. 修改顺序表中任意位置的值
(1)判断数组是否为空,为空则修改失败
(2)判断修改的位置下标Pos的合法性,数组已有的元素下标是[0,size-1],所以Pos也必须在该范围内,否则,修改失败。
(3)满足上述两个条件后,只需将下标为Pos的数组元素修改为设定的值即可。
//将顺序表seqlist中任意位置的元素修改为value
//Pos为修改位置的下标
void SeqListSet(SeqList *seqlist,int Pos,SeqListType value)
164 {
165 if(seqlist == NULL)
166 {
167 //非法输入
168 return;
169 }
170
171 if(seqlist->size == 0)
172 {
173 //顺序表已空
174 return;
175 }
176
177 if(Pos < 0 || Pos > seqlist->size - 1)
178 {
179 //修改位置不合法
180 return;
181 }
182
183 seqlist->data[Pos] = value;
184 return;
185 }
11. 在顺序表中查找指定元素所在的下标
(1)判断数组是否为空,为空则查找失败
(2)从下标为0开始,依次遍历整个数组,对比数组元素与要查找的值是否相同,若相同,则保存其下标的值,若遍历完整个数组,还未找到,说明要查找的值不在数组中。
(3)数组已有的元素下标是[0,size-1],设置计数器变量i,从下标为0开始遍历,逐次加1,直到i最大为size-1,即
初始:i=0;
遍历过程中:i<=size-1 => i<size,
代码如下:
//在顺序表seqlist中查找指定元素value所在的下标
//将下标放在Pos中
void SeqListFind(SeqList *seqlist,SeqListType value,int *Pos)
187 {
188 if(seqlist == NULL)
189 {
190 //非法输入
191 return;
192 }
193
194 if(seqlist->size == 0)
195 {
196 //顺序表为空
197 return;
198 }
199
200 int i = 0;
201 for(i = 0;i < seqlist->size;i++)
202 {
203 if(seqlist->data[i] == value)
204 {
205 *Pos = i;
206 return;
207 }
208 }
209 *Pos = -1;
210 return;
211 }
12. 删除指定元素,如果有重复,只删除一个
要删除指定元素,
(1)首先要找到该元素所在的下标,该过程可由上述已有的查找函数SeqListFind实现;
(2)下标找到后,就是删除该元素,该过程可以上述已有的删除指定位置元素函数SeqListErase实现;
代码如下:
//删除指定的元素,如果有重复,只删除一个
void SeqListRemove(SeqList* seqlist, SeqListType to_delete)
{
//指针判空
if(seqlist == NULL)
{
//非法输入
return;
}
int pos = -1;
SeqListFind(seqlist,to_delete,&pos);
SeqListErase(seqlist,pos);
return;
}
13. 删除指定的所有重复元素
该过程与只删除一个指定元素类似,
(1)先找到要删除元素所在的下标,然后在删除该元素;
(2)因为Find和Erase函数一次只能找一个,删一个。所以,需要不断的查找,删除,直到找不到为止。因为Find函数返回-1时,表示找不到要查找的元素,所以,可以以此为依据,判断是否查找完毕。
代码如下:
//删除所有重复的指定元素
void SeqListRemoveAll(SeqList* seqlist, SeqListType to_delete)
{
if(seqlist == NULL)
{
//非法输入
return;
}
int pos = -1;
SeqListFind(seqlist,to_delete,&pos);
while(pos != -1)//pos等于-1时,指定元素已找完
{
SeqListErase(seqlist,pos);
SeqListFind(seqlist,to_delete,&pos);
}
return;
}
在上述算法中,因为while循环语句的时间复杂度为O(n),而在while循环中有一个删除函数和查找函数,这两个函数的时间复杂度均为O(n),所以算法的时间复杂度为O(n^2),当数据量过大时,就会造成效率就相对较低,所以,对该算法进行优化,使其时间复杂度降为O(n)。
删除所有指定元素算法优化
要删除所有指定元素,就相当于使位于指定元素后的其他元素代替该指定元素,在其之前的其他元素不变。所以对数组元素进行重新赋值,当没有遇到指定元素时,数组元素不变,当遇到指定元素,使指定元素后面的其他元素占据指定元素的位置,此时,要对数组的长度减1,直到遍历完整个数组,并将所有的指定元素都代替。所有指定元素即被删除。
代码如下:
void SeqListRemoveAllEx(SeqList* seqlist, SeqListType to_delete)
{
if(seqlist == NULL)
{
//非法输入
return;
}
if(seqlist->size == 0)
{
//顺序表为空
return;
}
int count = 0;
int cur = 0;
int size = seqlist->size;//记录数组原来的长度
for(cur = 0;cur < size;++cur)
{
if(seqlist->data[cur] != to_delete)
{
seqlist->data[count++] = seqlist->data[cur];
}
else
{
seqlist->size--;//当遇到一个要删除的元素,数组长度减1
}
}
return;
}
在该算法中,时间复杂度即为O(n)。14. 统计顺序表中数组的元素个数
因为顺序表的成员变量size表示的即为数组的长度,即数组的元素个数,所以只需返回该变量的值即可。
代码如下:
//统计顺序表中的元素个数
int SeqListSize(SeqList* seqlist)
{
if(seqlist == NULL)
{
//非法输入
return -1;
}
return seqlist->size;
}
15. 判断顺序表是否为空
顺序表成员变量size表示数组的元素个数,如果size为0,顺序表即为空,此时,返回1。否则,不为空,返回0。
代码如下:
//判断顺序表是否为空
int SeqListEmpty(SeqList* seqlist)
{
if(seqlist == NULL)
{
//非法输入
return -1;
}
if(seqlist->size == 0)
{
return 1;//数组元素个数为0,返回1
}
else
{
return 0;//数组元素个数不为0,返回0
}
}
16. 冒泡排序顺序表 首先,了解一下冒泡排序的原理。例如,对于数组a[5]={9,5,6,3,7},进行升序排序:
(1)第一轮:从下标为0开始,比较到a[4],
使a[0]与a[1]比较,如果a[0]>a[1],则交换两元素,否则,不改变;
再使a[1]与a[2]比较,如果a[1]>a[2],则交换两元素,否则,不改变;
........
比较a[3]和a[4],如果a[3]>a[4],则交换两元素,否则,不改变;
这样,第一轮比较下来,a[4]即为数组中元素的最大值。
(2)第二轮:从下标为0开始,比较到a[3]
使a[0]与a[1]比较,如果a[0]>a[1],则交换两元素,否则,不改变;
这样,第而二轮比较下来,a[3]即为数组中除a[4]外的最大值。
.........................
(3)最后一轮:还从下标为0开始,使a[0]与a[1]比较,如果a[0]>a[1],则交换两元素,否则,不改变。
每一轮比较,都能使比较的元素中的最大值位于这几个元素最大下标处,例如,比较4个元素,可使这四个元素的最大值位于下标为3处。每进行一轮,少比较上一轮中的最大下标的元素。这样,经过上述几轮比较结束后,数组已经排好序。
(4)因为,数组中有5个元素,每一轮少比较一个元素,直到最后只剩一个元素,所以,共需4轮比较。
在每一轮比较中,两两进行比较,共需(5-1-轮数)次比较。所以套用两重循环即可完成排序,代码如下:
//冒泡排序顺序表
void SeqListBubbleSort(SeqList* seqlist)
{
if(seqlist == NULL)
{
//非法输入
return;
}
if(seqlist->size <= 1)
{
return;//数组元素个数小于等于1时,不需要排序
}
int count = 0;//记录比较的轮数
for(count = 0;count < seqlist->size-1;++count)
{
int cur = 0;//记录每一轮中比较的元素下标
int flag = 0;//标志位,如果数组已经有序,则flag=0
for(cur = 0;cur < seqlist->size-count-1;++cur)
{
if(seqlist->data[cur] > seqlist->data[cur+1])
{
swap(&seqlist->data[cur],&seqlist->data[cur+1]);//利用交换函数交换数组元素
flag = 1;//如果两数组元素不符合排序要求,说明数组元素不是有序的
}
}
if(flag == 0)
{
break;//在上一轮比较中,如果数组元素没有进行交换,说明数组已有序,此时,便不用在比较了
}
}
return;
}
//交换函数
void swap(SeqListType* a,SeqListType* b)
{
SeqListType tmp = *a;
*a = *b;
*b = tmp;
}
17. 利用回调函数优化冒泡排序 在上述函数中,要求升序排序。那如果要求降序排序,再如果,排序的元素不是字符型呢,而是其他不能用大于等于来判断元素大小的呢,比如字符串。这样每改变一次,就需要不断改变判断条件:if(seqlist->data[cur] > seqlist->data[cur+1])。这样代码的可维护性就比较差了。所以,将不同的判断条件封装成不同函数,每次判断的类型不同,从而调用不同的函数。
那么,跟回调函数有什么关系呢?这里,要了解一下回调函数的概念。回调函数是将函数的函数指针作为参数传入调用它的函数中,在调用它的函数中如果那里需要调用该函数,只需通过参数即函数指针来调用它即可,这就叫回调函数。
可能有人会说,为什么要将函数指针以参数的形式传入,而不是在要用到它是,直接调用?试想一下,如果一个函数中需调用某函数多次,而下一次改变条件,需要替换为调用另一个函数多次,这样就需要修改程序的很多地方。代码的可维护性比较差,所以以参数传入,如果调用不同类型的函数时只需改变实参,在调用它的函数中不用任何修改即可。这样,就大大提升了代码的可维护性。
这里,封装一个比较字符类型数据的函数:
int cmp_char(SeqListType a,SeqListType b)
{
if(a > b)
{
return 1;//参数a大于参数b,返回1
}
else if(a == b)
{
return 0;//参数a等于参数b,返回0
}
else
{
return -1; //参数a小于参数b,返回-1
}
}
然后,将该比较函数的函数指针以参数的形式传入冒泡排序的函数中:
这里,为简化代码量,将该函数指针类型做一个替换:
typedef int(*Cmp)(SeqListType a,SeqListType b);//Cmp为函数指针类型,指针指向一个有两个参数(类型均为SeqListType),返回值为int的函数
优化后的冒泡排序算法:
void SeqListBubbleSortEx(SeqList* seqlist,Cmp cmp)
{
if(seqlist == NULL)
{
//非法输入
return;
}
if(seqlist->size <= 1)
{
return;
}
int count = 0;
for(count = 0;count < seqlist->size-1;++count)
{
int flag = 0;
int cur = 0;
for(cur = 0;cur <seqlist->size-1-count;++cur)
{
if(cmp(seqlist->data[cur],seqlist->data[cur+1]) > 0)//如果回调函数的返回值大于1,说明参数1大于参数2,所以,需要交换
{
swap(&seqlist->data[cur],&seqlist->data[cur+1]);
flag = 1;
}
}
if(flag == 0)
{
break;
}
}
}
最后,在调用冒泡排序算法是时,只需将比较函数的函数指针(即函数名)cmp_char作为实参传入即可。如:
SeqListBubbleSortEx(&seqlist,cmp_char);
18. 选择排序顺序表 先了解一下选择排序的思想,对于数组a[4]={9,5,2,7},进行升序排序
(1)将a[0]依次与a[1]~a[3]进行比较,如果a[0]大于与其比较的数,则进行交换,所以,a[0]与其之后的各元素比较结束后,即为最小的数;
(2)将a[1]依次与a[2]~a[3]进行比较,与(1)类似,比较结束后,a[1]即为a[1]~a[3]中最小的数,而此时a[1]大于a[0];
(3)将a[2]与a[3]进行比较,如果a[2]>a[3],则进行交换,此时,数组所有元素已按升序排列。
(4)在比较过程中,设置一个边界值bound,bound从0开始,然后与其后的各元素进行比较,比较结束后;bound加1,再与其后的各元素进行比较,直到bound的值为数组倒数第二个元素的下标为止。在此过程中,[a[0],a[bound])始终为以排好序的序列,而[a[bound],a[3]]为待排序序列,当bound等于2时,所有元素均排好序。
(5)所以,利用两重循环即可实现,为增强代码的可维护性,这里,复用上述的比较函数作为回调函数。
代码如下:
//选择排序顺序表
void SeqListSelectSort(SeqList* seqlist,Cmp cmp)
{
if(seqlist == NULL)
{
//非法输入
return;
}
if(seqlist->size <= 1)
{
return;
}
int bound = 0;
for(bound = 0;bound < seqlist->size-1;++bound)
{
int cur = bound + 1;
for(;cur < seqlist->size;++cur)
{
if(cmp(seqlist->data[bound],seqlist->data[cur]))
{
swap(&seqlist->data[bound],&seqlist->data[cur]);
}
}
}
}