顺序表的相关操作

本文详细介绍了顺序表的基本概念、存储结构及其在C语言中的实现方式,包括初始化、增删查改等基本操作,并探讨了排序算法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        线性表是具有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[2]和a[3],如果a[2]>a[3],则交换两元素,否则,不改变;

这样,第而二轮比较下来,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]);
            }
        }
    }
}







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值