顺序表中的运算无非四种,增删改查:
- 增:即向顺序表中插入数据,大部分情况下都是向一个有序表中插入,要是无序的话就先排序。
- 删:删除顺序表中的一个或多个元素,通过数据覆盖来消除原来数据。
- 改:找到数据,修改数据。本质上还是查找。
- 查:用查找算法查找数据。
相信有关于查找的算法大家已经听过很多了,什么折半查找、散列查找之类的。这些查找算法都很经典,所以在网上很容易找到,有时间的话我会总结一下各种查找的方法。今天着重来说一说删除和插入的算法。
先说顺序表的删除算法:
题目1:设数据集合中的数据元素不重复,有一个数据集合A,任意输入一个关键字key,在数据集合中删除数据域值为key的数据。
其实顺序表的删除是比较麻烦的一个算法,删之前还要先查找一番,找到了还要把它后边的元素向前挪,挪完后还要将结尾处处理下。大部分删除都是这个套路,那么我们就按照这个套路来写一遍代码:
void delete(int A[],int key,int& n)
{
int i,j;
for(i=0;i<n&&A[i]-key;i++); //查找key值元素
if(i>=n)
cout<<"not found"<<endl;
else{
for(j=i;j<n-1;A[j]=A[j+1],j++);//若找到,将该元素后边的值向前覆盖
--n;//数组长度减1
}
}
以上为在顺序表中删除一个元素的完整套路。先查找,再覆盖,最后收尾。这是一个最典型的删除例子,也是最简单的。下面我们修改一下题目。
题目2:设数据集合中的数据元素有重复值,有一个数据集合A,任意输入一个关键字key,在数据集合中删除数据域值为key的数据。
分析:这次的难点在于数据元素有重复,可能要删除的元素在数组中有多个。那么假如我们要删除多个元素,能用到的方法是什么呢?按照上题的思路,我们可以先用第一层循环查找到第一个key,然后用第二层循环将key后边的元素前移,接着又回到第一层循环查找到第二个key,再用第二层循环将key后边的元素前移。。。。。。时间复杂度O(n^2),可以解决问题,但是算不得一个高效算法。
可不可以尝试着用一层循环就将所有key值删除呢?我们之所以习惯性的想用两层循环,是因为我们需要在找到一个key值元素并删除后,还能在原位置上继续找。也就是说,需要两个可以指向元素的下标。那么我们的思路就可以这样:
用两个下标i,j来指着这个数组的头元素,要求
- 每比较一个元素i就加1,i指着正在比较的元素。
- 每次比较,i指向的元素值若不为key,j加1;否则j不变。
例如,本次循环中,i和j值相同,都指向一个和key相同的元素,那么下次循环,i加1,j不变,j仍然指向那个待删除的值。那么怎么删除它?直接将i指向的非待删元素覆盖过去就行了。覆盖之后,j指向的元素不为key了,j也就可以继续移动了。有了这个思路,我们就用代码说话:
void delete(int A[],int key,int& n)
{
int i,j;
for(i=j=0;i<n;i++)
if(A[i]!=key) //A[i]不为待删元素时,将A[i]赋值给A[j],然后j加1
A[j++]=A[i];
n=j;//j为此时数组元素个数
}
还可以再简化:
void delete(int A[],int key,int& n)
{
int i,j;
for(i=j=0;i<n&&(A[i]==key||(A[j++]=A[i],1));i++);
/*
//(装逼利器)或条件运算符只有在A[i]==key为假时才会执行后边语句;
//逗号运算符的结果为逗号后的表达式值,这里是防止数组中有值为0的元素
*/
n=j;
}
以上的问题中都是要求删除特定值key,那么接下来问点不一样的:
题目3:设数据集合中的数据元素有重复值,有一个数据集合A,元素按升序排列。删除数组中重复的元素。
其实如果理解了上一个题目用到的方法,那么做这道题也不会觉得难。因为这两道题的方法是一样的。
分析:用两个数组下标i,j指向数组头元素,创建一个循环,在循环中,i每次增加1;j只有在i指向的值a与它指向的值b不同时,j加1,然后将a赋值给b。循环结束,j+1为此时数组元素个数。
void delete(int A[],int& n)
{
int i,j;
for(i=j=0;i<n&&(A[i]==A[j]||(A[++j]=A[i],1));i++);
n=j+1;
}
注意题目2和题目3中的j++和++j:
- 题目2中用j++,是为了删除当前元素,先赋值,再后移。
- 题目3中用++j,是为了删除后边的重复元素(当前元素留着),先后移,再赋值。
如果再有这种题目:
题目4:设数据集合中的数据元素有重复值,有一个数据集合A,元素无序。删除数组中重复的元素。
只需要将数组排序一遍,然后再用题目3的方法解决就好了。关于顺序表的删除我们就告一段落,如果还有别的高效解法我会继续扩充。
再说顺序表的插入算法:
产生插入问题的顺序表一般都是有序表(无序的话就随便插了)。插入的过程应该说是删除的逆过程,它的套路就是:先从后向前找到应该插入的位置(可以在这个过程中把不合适的元素向后移动),再将数值插入,最后收尾。因为在数据插入的时候,插入位置之后的原数据需要向后移动,所以建议查找插入位置时从后向前查找,在查找的同时将元素后移,减少不必要的循环。
举个例子吧:
题目1:有一个数据集合A,元素升序排列。有一个关键字key,将key插入集合,使之仍然有序。
代码:
void insert(int A[],int key,int& n)
{
int i;
for(i=n-1;i>0&&A[i-1]>key;A[i]=A[i-1],i--);
A[i]=key;
}
这里小心数组越界的情况,除非可以根据上下文明确数组的容量大于n,否则只能将数组最后一个元素覆盖,不能后移。
我们接触的大部分情况下的插入,都是一个数据插入到一个数组中。那么当一个数组需要插入到另一个数组时应该怎么办?
题目2:有数据集合A、B,元素皆升序排列。将两数据集合合并,使之仍然有序。
解法:建立两层循环,第一层选定数组A某一元素,第二层查找B中合适位置将A中元素插入。时间复杂度O(n^2),而且数组越界(额外开辟空间就可以了)。
这种解法并不好,因为时间复杂度还可以再降低。既然已经是升序排列了,那么可以用两个变量指向这两个数组元素,对元素遍历一次的同时进行选择,将选择的数据放入额外的数组C中。
但是另一个问题是,什么时候选择A的元素,什么时候选择B的元素?可以肯定的是,A和B肯定有一个先遍历完。那我们划分一下情况:
一. A没遍历完
1. B没遍历完
(1). A[i]
(2). A[i]>=B[j]: C[k]=B[j];
2. B遍历完
C[k]=A[i];
二. A遍历完
C[k]=B[j]
可以看到根据划分的情况可进行的两种操作。如果将条件合并,就可以在一次遍历中解决问题。
代码:
void combine(int A[m],int B[n],int C[m+n])
{
int i=0,j=0,k=0;
while(i<m||j<n)
{
if(j==n||(i<m&&A[i]<B[j]))//该算法相当于根据结果划分情况,再由情况选择执行哪个指令
C[k++]=A[i++];
else
C[k++]=B[j++];
}
}