九、顺序表和单链表的对比分析

1、如何判断某个数据元素是否存在于线性表中?

find()操作:

  • 可以为线性表List增加一个查找操作

  • int find(const T& e)const;

    • 参数:待查找的数据元素

    • 返回值:

      大于0:数据元素在线性表中第一次出现的位置

      -1:数据元素不存在

针对基础数据类型,首先在顶层父类List中增加一个虚函数 virtual int find(const T& e) const = 0;,然后在各子类中实现这个函数

// 顺序表中的实现 SeqList.h
int find(const T& e) const  // O(n)
{
    int ret = -1;
    for(int i = 0; i < m_length; i++)
    {
        if(m_array[i] == e)
        {
            ret = i;
            break;
        }
    }

    return ret;
}

// 单链表中的实现 LinkList.h
int find(const T& e) const
{
    int ret = -1;
    int i = 0;
    Node* node = m_header.next;
    while(node)
    {
        if(node->value == e)
        {
            ret = i;
            break;
        }
        else
        {
            node = node->next;
            i++;
        }
    }
    return ret;
}

针对自定义类类来说

class Test
{
    int i;
public:
    Test(int v = 0)
    {
        i = v;
    }
};
int main()
{
    Test t1(1);
    Test t2(2);
    Test t3(3);
    LinkList<Test> list; 
    
    return 0;
}

// 会报一个错,错误的地方在LinkList.h
int find(const T& e) const
    {
......
        while(node)
        {
            if(node->value == e)	// 这句话报错
        }
......
    }
/* 报错的原因是:
node->value 和 e都是Test对象,并没有重载Test类的"=="操作符,在这里编译器不知道如何去比较这两个Test对象
*/

解决方案1:在Test类中直接重载相等比较操作符==

class Test
{
    int i;
public:
    Test(int v = 0)
    {
        i = v;
    }
    bool operator == (const Test& t)
    {
        return (i == t.i);
    }
};	

这样就可以编译通过,但是这样写存在着一个问题,我们的本意是定义一个单链表对象,保存的对象元素是Test对象,并没有进行实际的查找,也就是说还没想用==操作符来比较,我们就要去在Test类中重载==操作符,这意味着,但凡我们想要定一个这样的保存自定义类类型的单链表对象,我们就必须在自定义类类型中重载==操作符。这样find()查找函数的便利性还没有体现出来,编译错误先出现了,find操作使我们自定义类类型的时候也更加麻烦了。
但是find操作还是应该存在的,需要解决的问题是,使find函数依然存在,但是自定义类类型的时候也不需要每次都重载,只在需要用到查找函数的类类型时再重载。

思路:在顶层父类中实现操作符==!=的重载,定义类时,都继承于这个父类,这样类模板中的find()函数实现就可以通过编译。如果自定义类类型需要用到这个find()函数时,再重载操作符==实现相等比较逻辑。

// Object.h
class Object
{
public:
...
    bool operator == (const Object& obj);
    bool operator != (const Object& obj);
...
};
// Object.cpp
bool Object::operator == (const Object& obj)
{
    // 默认实现的方式最好就是通过比较地址
    return (this == &obj);
}
bool Object::operator != (const Object& obj)
{
    return (this != &obj);
}

// 使用的时候,就需要在定义自定义类类型的时候继承Object父类
class Test : public Object
{
    int i;
public:
    Test(int v = 0)
    {
        i = v;
    }
};

// 这样,这句话就不会出现编译错误了
LinkList<Test> list;

// 下面再考虑查找find函数
    Test t1(1);
    Test t2(2);
    Test t3(3);
    LinkList<Test> list;

    list.insert(0,t1);
    list.insert(0,t2);
    list.insert(0,t3);
    
	list.find(t3);
// 查找的依据应该是Test内的i的值,此时就需要在Test类中重载"=="操作符, 提供具体的相等比较逻辑
class Test : public Object
{
public:
...
    bool operator ==(const Test& t)
    {
        return (i == t.i);
    }
...
};

顶层父类中重载的== != 只是提供了一个默认的相等比较符的实现方式,是为了类模板中编译通过,针对某个具体的自定义类类型的时候,如果需要用到==!=时,需要在自定义类中重载这两个操作符,因为默认的逻辑不是我们需要的相等或不等比较的实现逻辑。

2、顺序表和单链表的对比分析

操作 SeqList LinkList
insert O(n) O(n)
remove O(n) O(n)
set O(1) O(n)
get O(1) O(n)
find O(n) O(n)
length O(1) O(1)
clear O(1) O(n)

从时间复杂上来说,单链表和顺序表比起来并不高效,但是工程上单链表用得比顺序表更多

顺序表的整体时间复杂度比单链表要低,那么单链表还有使用价值吗?

实际工程开发中,时间复杂度只是效率的一个参考指标

  • 对于内置基础类型,顺序表和单链表的效率不相上下
  • 对于自定义类类型,顺序表再效率上低于单链表

效率的深度分析

  • 插入和删除:

    • 顺序表:涉及大量数据对象的复制操作
    • 单链表:只涉及指针操作,效率与数据对象无关

    基础内置类型如int,顺序表的复制操作只涉及到4个字节,而单链表涉及的是指针操作,4字节或8字节,两者效率相差不大

    自定义类类型:非常复杂的类类型,涉及深拷贝的类类型,采用顺序表来存储,但凡到插入和删除一个对象,就要进行很多次大数据对象的复制,还是深拷贝对象的复制,这个大数据对象占用的空间可能会很大,此时顺序表的插入和删除,效率极低;如果采用单链表来存储的话,就只涉及指针操作,效率和具体的数据对象类型是无关的

  • 数据访问

    • 顺序表:随机访问,可直接定位数据对象,内部实现是用原生数组来做的,定位的时候基本不耗时,时间复杂度是常量
    • 单链表:顺序访问,必须从头访问数据对象,无法直接定位

工程开发中的选择:

  • 顺序表:
    • 数据元素的类型想对简单,不涉及深拷贝
    • 数据元素相对稳定,访问操作远多于插入和删除操作
  • 单链表:
    • 数据元素的类型相对复杂,复制操作相对耗时
    • 数据元素不稳定,需要经常插入和删除,访问操作比较少

3、小结

线性表中元素的查找依赖于相等比较符==

顺序表适用于访问需求量较大的场合(随机访问)

单链表适用于数据元素频繁插入删除的场合(顺序访问)

当数据类型相对简单时,顺序表和单链表的效率不相上下

原文地址:https://www.cnblogs.com/chenke1731/p/9500703.html