不得不知道的实用数据结构算法(一)

新坑说开就开,就是这么随(不)心(知)所(死)欲(活),这个系列实际上是针对数据结构中的一些低时间复杂度的常用模板or思考方式的总结,ok,今天先进行的是线性表。

线性表

定义:

1
2
3
4
5
6
7
8
9
# include <stdio.h>
typedef ElemType int;

struct typedef SeqList
{
ElemType *data; // 按道理这里应该有很多实现方法,比如直接上一个数组,通过宏定义最大数组长度
int size;
int capacity;
}SeqList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <stdio.h>
# define MAXSIZE 100

typedef ElemType int;

struct typedef SeqList
{
ElemType data[MAXSIZE];
int length;
}SeqList;

void init(SeqList *l)
{
l->length=0;
}

秉承着不给自己找麻烦,后边就不用动态分配空间的代码了,虽然在我初学的时候觉得动态分配真的很优雅(起码不怕爆了)

OK,然后迅速的过一下基本操作以及其操作的流程以及弊端(懒得写了):

删除:删除一个元素,要将后边的元素都移过来,实际上也就是说复杂度是O(n)

遍历:这不必多说,自然O(n)

随机存取:这是顺序表的强项

插入:和删除其实一致,但是要求后移后面的元素

然而很多算法题会颠覆你对基本操作的认识,也就是会让你优化一种插入操作or删除操作,使一个本来暴力解时间复杂度为O(n2)的算法优化为时间复杂度为O(n)的算法

img

我们先举个例子:

现在有一个顺序表,其中有元素n个,现在我希望删除其中的所有偶数元素

我们从直觉上很容易写出这样一个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void dele_even(SeqList *l)
{
if (l==NULL)
{
printf("顺序表未初始化");
return ;
}

for (int i = 0; i < l->length; i++)
{
if (l->data[i]%2==0)
{
delete(l,i); #时间复杂度是O(n)
}
}
}

简单瞅一眼,很明显可以看出这是个O(n2)的时间复杂度的算法,但是有没有一种可能,这个算法可以被优化到O(n),这就是技巧一:有效的循环合并

技巧一:有效的循环合并

我们可以分析一下刚才删除的过程,其实有一些动作是多余的,我们其实没有必要去移动一个可能会被删除的元素:当我们找到一个偶数时,实际上后边的元素中依旧可能存在偶数,那么我们将这些偶数移动到前面就是一个多余的动作,因为以后依旧要被删除,其后边的元素仍旧面临的再一次循环的境地,因此我们是不是可以通过某种手段避免无效的遍历移动呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void dele_even_good(SeqList *l)
{
if (l==NULL)
{
printf("顺序表未初始化");
return ;
}

int m=0; //用于存储找到了多少偶数

for (int i = 0; i < l->length; i++)
{
if (m!=0 && i-m>0)
{
l->data[i-m]=l->data[i];
}

if (l->data[i]%2==0)
{
m++;
}
}
l->length=l->length-m;
}

这里采用了一边删除一边寻找的方式,将两个循环合并到了一起,从而有效的降低了时间复杂度。这种思想很适合于使用在处理一些你想无脑调API的时候的优化。

技巧二:为什么不试试多次就地逆置呢

众所周知,就地逆置是处理一些奇奇怪怪需要一大堆循环的题目的利器,比如互换两块顺序表的位置:

有一个数组A[m+n],要求将这前m个元素和后n个元素前后互换那么就地逆置就很有用了,我们只需要整体一次,再局部两次:

(1,2,3,……,m,m+1,……,m+n)

第一次就地逆置:(m+n,……,m+1,m,……,3,2,1)

第二次局部逆置:(m+1,……,m+n,m,……,3,2,1)

第三次局部逆置:(m+1,……,m+n,1,2,3,……,m)

漂亮!

技巧三:空间换时间,随机存取之光

随机存取就是一个很爽的性质,这意味着只要我们找到了元素的index就可以迅速的锁定元素,比如现在需要给一个顺序剔除重复元素,那么我怎么知道元素重复了呢?我们可以再来一个顺序表通过哈希的方式得到,举个例子:

假如我们扫描到了一个元素3,那么我们就将hash[3]=1(即新创建的顺序表)的位置,以此类推,那么当我们每扫描到一个元素的时候,只需要其比较hash[data]是否值为1,如果为1那么就重复了,再去执行我们前面做好的删除算法即可。

总结

还有一些顺序表的算法题目,就比较没规律了,需要通过画图,手动运算一下,来找一下其中的规律,嗯,这些应付普通考试已经够了,剩下等我刷刷Leecode再行总结。