【线性表3】线性表的链式实现:单链表

简介

特点
1、 用一组地址任意的存储单元 存储数据元素。存储单元地址 可连续,也可不连续。 为了形成逻辑线性结构,每一个结点 除了保存需要存储的数据外,还需要保存逻辑上相邻的下一个结点的地址。
2、链表由n个类型相同的结点通过指针链接形成线性结构。结点由数据域和指针域组成。 数据域用于存储结点代表的数据,指针域存储结点的后继结点地址。
3、 链表不支持随机访问。有n个结点的线性表,访问某个结点的平均时间复杂度为O(n/2),最坏为O(n) 。而数组支持随机访问,他的访问时间复杂度为O(1)
         
 
优点:插入和删除操作无需移动元素,只需修改结点的指针域。这点恰巧是顺序表(如ArrayList和数组)的缺点。
缺点:访问元素时,不支持随机访问。访问第n个数据元素,必须先得到第n-1个元素的地址, 因此访问任何一个结点必须从头结点开始向后迭代寻找,直到找到这个目标结点为止。 
 
 
结点:由数据域和指针域构成。数据域用来存储数据元素,而指针域用来保存逻辑上相邻的下一个结点的地址。
头结点:也叫哑结点(dummy node)。为了方便,一般情况下,我们都在链表的第一个逻辑位置上使用一个头结点。它的数据域不保存数据,而指针域保存表的第一个结点的地址,他相当于一个标记结点。

 

带头结点的单链表的示意图

 

 

 如下是一个保存 char类型,数据元素依次为 【'A' , 'B' , 'C'】的,带头结点的链表的结构图。

 

 

链表的主要操作

  • 追加结点(尾插法和头插法追加)
  • 插入结点(在指定的索引位置插入一个结点)
  • 删除结点(删除一个指定索引的结点)
  • 访问结点(get/set)
  • 遍历结点

 代码实现

 

 初始状态

使用成员字段headNode 代表头结点(ListNode结构体对象),初始状态指针域为NULL。
使用成员字段pLastNode保存链表的最后一个结点地址,这样在执行append操作时,就不必循环了。
使用成员字段size保存链表实时长度。
 
在执行clear操作后,链表会恢复到这个状态。

 

#include<iostream>
#include<cstdlib>
#include<stdexcept>   
using namespace std;
struct ListNode
{
    int element;
    ListNode* next;
    
    ListNode(int e=0,ListNode* nxt=0):element(e),next(nxt)
    {
    }
    
};
class LinkeList
{
private:
    ListNode  headNode;      //头结点
    ListNode* pLastNode;     //保存最后一个结点的地址
    int size;                //表实际长度
    
public:
    LinkeList():headNode(0,0),pLastNode(0),size(0)
    {
        pLastNode = &headNode;
    }
    
    ~LinkeList()
    {
        //析构函数:释放所有的数据结点
        clear();
    }
    /*
    * 功能:删除索引为index 的结点  
    * 时间复杂度O(n)
    */
    bool remove(int index)
    {
        ListNode *p = &headNode;
        ListNode *p_delete;
        int i=0;
        
        if(index <0) return false;
        
        //循环用于获取 待删除结点的前一个结点的指针 。
        //用 i<index 去限制循环执行的次数
        while(p!=0 && i<index)
        {
            p= p->next;
            i++;
        }
        
        //退出循环后,合法情况下,p为待删除结点的前一个结点的指针
        //因此p 和 p->next 都不能为空 ,否则就是因为参数index不合法
        if(p==0 || p->next==0)
            return false;
            
        
        p_delete = p->next;    
        if(p_delete->next==0)
            pLastNode = p;   //如果删除的是最后一个结点,则更新pLastNode
        p->next = p_delete->next;
        delete p_delete;
        size --;
        
        
        return true;
    }
    
    
    /*
    * 功能:将新元素e包装为结点,插入到索引为index 的地方。
    * 时间复杂度O(n)
    */
    
    bool insert(int index , int e)
    {
        ListNode *p = &headNode;
        ListNode *p_new;
        int i=0;
        
        if(index <0) return false;
        
        //循环用于获取 待插结点的前一个结点的指针
        //用 i<index 去限制循环执行的次数
        while(p!=0 && i<index)
        {
            p= p->next;
            i++;
        }
        
        //退出循环后,合法情况下,p为待插结点的前一个结点的指针
        //因此p 不能为空 ,否则就是因为参数index不合法,index过大 。
        //但是p->next可以为空,如果是空,则相当于末尾追加append 。如果不为空,则是在中间位置插入。
        if(p==0) return false;
        
        p_new = new ListNode(e,p->next);  
        if(p->next == 0)
            pLastNode = p_new;   //更新pLastNode
        p->next = p_new;
        
        size++;
        
        
        return true;
    }
    
    
    
    /*
    * 功能:在链表末尾追加一个元素。
    */
    void append(int e)
    {
        ListNode*new_node = new ListNode(e,0);  //构造新结点
        
        pLastNode->next = new_node;
        pLastNode = new_node;    
        size++;
    }
    
    int length()const
    {
        return size;
    }
    
    int indexOf(int e)const
    {
        ListNode *p = headNode.next;
        
        for(int i=0;p!=0;++i,p=p->next){
            if(p->element == e) return i;
        }
        return -1;   // not found
    }     
    
    bool isEmpty()const
    {
        return size == 0;
    }
    
    
    /*
    * 功能:删除所有的数据结点,清空表
    */
    void clear()
    {
        ListNode*p = headNode.next;
    
        ListNode* t;
        while(p!=0){
            t = p;
            p = p->next;
            delete t;
        }
        //回归初始状态
        headNode.next= 0;
        pLastNode = &headNode;
        size = 0;
    }
    
    void show()const
    {
        ListNode*p = headNode.next;
        
        cout<<"[";
        while(p!=0){
            if(p!=headNode.next)
                cout<<',';
            cout<<p->element;
            p=p->next;
        }
        cout<<"]";   
    }
    
    int operator[](size_t index)const
    {
        ListNode*p = headNode.next;
        int i=0;
        
        if( index <0 || index >= size  )
            throw std::out_of_range(0);
        
        while(p!=0 && i<index){
            p = p->next;
            i++;        
        }
        return p->element;
        
    }
    int& operator[](size_t index)
    {
        ListNode*p = headNode.next;
        int i=0;
        
        if( index <0 || index >= size  )
            throw std::out_of_range(0);
        
        while(p!=0 && i<index){
            p = p->next;
            i++;        
        }
        return p->element;
    }
    
};
int main()
{
    LinkeList list;
    
    
    list.append(1);
    list.append(2);
    list.append(6);
    
    
    list.insert(2,3);
    list.insert(3,4);
    list.insert(4,5);
    list.insert(5,5);
    
    cout<<"len = "<<list.length()<<endl;
    
    
    list.show(); cout<<"\n";
    
    list.remove(6);
    list.remove(5);
    
    list.show(); cout<<"\n";
    
    list.append(100);
    
    list.show(); cout<<"\n";
    
    return 0;
}

 

获取指定索引处的元素

 链表不支持随机访问。因此时间复杂度为O(n)。这是单链表不可避免的缺点。 

 

int& operator[](size_t index)
{
        ListNode*p = headNode.next;
        int i=0;
        
        if( index <0 || index >= size  )   //索引越界
            throw std::out_of_range(0);
        
        while(p!=0 && i<index){   //循环找到结点的指针
            p = p->next;
            i++;        
        }
        return p->element;
}

 

 

插入元素

插入元素前,需要先获取待插入位置的前一个结点的地址。在插入时,先连接后结点,在连接前结点,这样就避免使用临时变量了。
在已知待插入位置的前一个结点的地址情况下,时间复杂度为O(1)

 

删除元素 

删除元素前,需要先获取待删除位置的前一个结点的地址。
在已知待删除位置的前一个结点的地址情况下,时间复杂度为O(1)

 

 

小提示

1、从理论上说,链表的优点就是因为他插入和删除等更改元素位置的操作很高效,时间复杂度为O(1) ,但实际上,由于我们在使用线性表时,是基于索引的,我们总是用索引标识一个结点,而不是他的地址,因此这就削弱了链表的优势。例如为了删除索引为n的结点,我们必须从头结点开始循环,找到索引为n-1的结点的地址,然后才能执行删除操作。所以从这方面看,实际操作时时间复杂度依然是O(n)。但是,修改指针比移动大量结点元素快多了,所以通常这也不是太大的问题。
 
 
2、链表是不支持随机访问的,因此,对于链表,如果我们想对所有的结点执行某种操作,不应该使用传统的 for 循环,而应该使用迭代器,因为迭代器只需完成一次性循环,避免反复循环。下面是一Java集合框架中的LinkedList做测试,可以明显感受二者的差距,使用迭代器遍历比使用for循环快近10倍。

 

public class DataStructure
{

    public static void main(String[] args)
    {
        LinkedList<Integer> list = new LinkedList<Integer>();
        
        for(int i=0;i<100000;++i)
        {
            list.add(i);
        }
        
        useIterator(list);
        //useForLoop(list);
        
    }
    
    
    //耗时:4950ms
    public static void useForLoop(LinkedList<Integer> list)
    {
        long s = System.currentTimeMillis();
        
        for (int i = 0; i < list.size(); i++)
        {
            System.out.println(list.get(i)); 
        }
        long e = System.currentTimeMillis();
        
        System.out.println("耗时:"+(e-s) + "ms");
        
    }
    
    //耗时:546ms
    public static void useIterator(LinkedList<Integer> list)
    {
        
        long s = System.currentTimeMillis();
        
        for(Integer i:list)
        {
            System.out.println(i);
        }
        long e = System.currentTimeMillis();
        
        
        System.out.println("耗时:"+(e-s) + "ms");
    }
}
测试代码

 

 

练习

1、实现单链表的选择排序

2、实现单链表的头插法添加数据

3、实现反转单链表(空间复杂度为O(1))

 

/*代码省去了前面已经实现的部分*/
class LinkeList
{

//...

public:

//...     
    //选择排序 
    void selectSort()
    {
        ListNode *aim = headNode.next;  
        ListNode *min;
        ListNode *p;
        int t;
        
        if(aim==0) return;   //空表无需排序 
        
        while(aim->next!=0)
        {
            min = aim;      //假设当前比较对象结点aim是最小的 
            p = aim->next;
            while(p!=0)
            {
                if(min->element  >  p->element){
                    min = p;
                }
                p = p->next;    
            }
            if(min != aim)  //最小元素易主了 
            {
                t = min->element;
                min->element = aim->element;
                aim->element = t;        
            }
            
            aim = aim->next;    
        }
    
    }
    
    
    //头插法添加数据 :将数据添加为链表的第一个结点中 
    void addFirst(int e)
    {
        ListNode*new_node = new ListNode(e,headNode.next);  //构造新结点 
        headNode.next = new_node;
        size++;
    }
    
    //反转单链表 
    void revserse()
    {
        ListNode* pre=0;
        ListNode* cur = headNode.next;
        ListNode* nxt = cur->next;
        
        if(size < 1) return ;
        
        while(nxt!=0)
        {
            cur->next = pre;
            pre = cur;
            cur = nxt;
            
            nxt = nxt->next;    
        
        }
        
        cur->next = pre;
        headNode.next = cur;
           
    } 
    
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值