顺序表,链表总结

自学过数据结构,现在老师又讲了一遍,稍微总结一下。

静态存储的顺序表:

  1.  初始化

  

#include <iostream>
#include <algorithm>

using std::cout;
using std::sort;

template <class T>
class List
{
private:
    int max_size;//数组的最大空间;
    int len; //当前有效元素的个数,从1开始计数;
    T * lis;
public:
    List(int m_Max = 10);
    int get_max(){return max_size;};//获取最大空间
    int get_len(){return len;};//获取元素长度
    bool append(T m);//在末尾插入元素;
    int find(const T& n);//遍历查找,返回元素n的下标,若不在元素中,则返回-1;
    int binary_find(const T&);//二分查找
    bool remove (const T& n);//删除元素n;
    bool exchange_del(const T& n);//交换删除,删除后元素顺序改变,时间复杂度O(1)
    bool insert(const int & loc, const T & n);//在i位置插入n;
    void show();//打印数组元素
    ~List();
};

 

  2.搜索

  2.1顺序搜索

template <class T>
int List<T>::find(const T& n)//必须是const,否则find(1)报错:“非常量的引用必须是左值。”
//隐式转化引起的,T& n = T(5),对常量修改了,禁止!
{
    int i = 0;
    while(i < len && lis[i] != n )i++;
    return i < len ? i : -1 ;//i < len说明说明lis[i]==n
}

  2.2二分搜索

  

template <class T>
int List<T>::binary_find(const T& n){
    sort(lis, lis + len);//排序
    int min = 0, max = len - 1, mid = (min + max) / 2;
    while(min <= max){
        if(lis[mid] == n)return mid;
        else if(lis[mid] < n)min = mid + 1;//在mid + 1,max之间查找
        else max = mid -1;//在min, mid -1之间查找
        mid = (min + max)/2;
    }
    return -1;
}

 

  3.插入

  插入分为在尾部插入和中间某个位置插入。

  在尾部插入:不需要移动元素,时间复杂度O(1);

   

template <class T>
bool List<T>::append(T n)
{
 if(len == max_size)//存储空间已满
    return false;
 lis[len++] = n;//插入,有效元素个数加1
 return true;
}

如果在中间某个位置插入,则需要把当前元素和后面的元素整体向后移动。(注意,当前元素也要向后移动,要不然就是插入后,当前元素就被删除了)i >=1 且 i <= len,当然了,如果把上面的情况包含进去,1<= i <= len + 1;

时间复杂度分析:第一个位置插入,则需要移动移动len个元素,第二个位置插入,移动len - 1个元素。由于插入每个位置的概率是相等的,

时间复杂度:len + len -1 + len - 2 + len -3  +  …… +  len -len = (0 + 1 + 2 +……+ len) / len = (len + 1) / 2

template <class T>
bool List<T>::insert(const int & loc , const T & n)
{
    /*
    loc:插入的位置,从1开始;
    n:插入的数;
    */
    if(len == max_size || loc < 1 || loc > len + 1)return false;
    for(int j = len -1; j >= loc-1; j--)lis[j+1] = lis[j];//loc = len + 1 时,循环不执行,因此不需要分开讨论
    lis[loc-1]=n;
    len ++;//这个常常忘记
    return true;
}

 4.删除算法

4.1移动删除

  删除的话,其实基本的思想就是覆盖,当删除某个元素时,只需要把后面的元素向前移动,覆盖这个元素。

  时间复杂度:删除第一个,后面len -1个元素前移,删除第二个,后面len -2个元素前移,……

  len - 1 + len -2 + ……+ 0 = (len -1)/2  

template <class T>
bool List<T>::remove(const T& n)
{
    int index = binary_find(n);//查找n是否在数组中
    if(index == -1)return false;
    for(int i = index + 1; i < len; i++)lis[i-1]=lis[i];
    len--;
    return true;
}  

 4.2交换删除

我们知道,删除最后一个元素的时间复杂度是O(1),如果我们把要删除的元素与最后一个元素交换,接着删除最后一个元素。当然,这样元素的顺序要改变。

template <class T>
bool List<T>::exchange_del(const T& n){
    int index = binary_find(n);
    if(index == -1)return false;
    //n与最后一个元素交换
    T temp = lis[index];
    lis[index] = lis[len-1];
    lis[len-1] = temp;
    //删除最后一个元素
    len--;
}

 

  5.扩容:可以采用new一个新对象,把原对象的内容复制过去,这个对象的存储空间是原来空间的n倍,vector中这个n是2。

  6.应用

    实现集合类的并,交运算

 

动态存储的顺序表:

  vector源码:https://www.kancloud.cn/digest/mystl/192550

  上次在群里看到有个人写了if(vector().size()-1 == -1),结果出了bug,半天找不出来,结果呢,看了源码才知道vector.size()的类型是size_t,

  她的定义是typedef unsigned int size_t.初始化的vector.size()为0,0-1结果溢出。

   (有点没看懂,以后再来瞅瞅)

单链表:针对节点与链表的关系,大致有一下实现方式。

Node:节点类
LinkList:单链表类
.嵌套类
  LinkList{
  Node
}
2.复合类 LinkList{ friend Node; }
LinkList可以访问Node内的数据成员,但是Node不一定可以访问LinkList
3.公有继承方式,
LinkList:public Node{

}
但是这样继承好像在逻辑上有点说不过去。
继承是 is a 关系,也就是说强调子类是一种父类,但是子类又有自己的特点。
has a关系 我们常常采用的是把一个类作为另一个个类的数据成员
is like a : like,像而不是is,这是父类常常是包含公有属性或方法的类。
use a : 我们采用友元函数来实现类与类之间的通信问题。
4.结构体+类采用组合的方式
  我们可以在LinkList中声明头节点,初始化节点后,节点把头结点指向首节点。

  

//定义节点类
template <class T>
class Node
{
public:
    T data;//数据域
    Node<T>* next;//指向下一个节点的指针
//此处即使不初始化,next = NULL;NULL即是0,不可访问的内存
public:
    Node()
    {
        next = nullptr;
    }
};

 

#include "Node.h"
#include <cstdlib>
#include <ctime>
#include <iostream>

using std::cout;
using std::endl;

//定义一个链表类LinkList
//规定:第0个节点表示头结点

template <class T>
class LinkList: public Node<T> 
{
private:
    Node<T>* head;//指向头结点的指针
    int length;
public:
    LinkList();//生成头结点
    bool get(int i, T& data);//获取第i个节点的数据域的值
    bool insert(int i, Node<T>* node);//在第i个位置后面插入节点
    bool del(int i);//删除第i个节点
    void head_create();//整表创建,头插法
    void tail_creat();//尾插法
    void show();//遍历链表,并打印所有元素
    void reverse();//链表反转
    void rev();//构造法实现链表反转
    int len();//获取链表长度,便于之后遍历;
    ~LinkList();//释放内存
};

 

 插入:

1.头插法:就是在每一次插入都把节点放在头结点的后面,这个就像是银行排队的时候,新来的人总是要挤到第一个银行窗口去办理业务。好像可以用来实现栈。时间复杂度O(1)
template <class T>
void  LinkList<T>::head_create(T val)
{

    Node<T>*  node = new Node<T>(val);
    node -> next = head -> next;
    head -> next = node;
    length++;    
    //思考:我们在书写循环语句时,不要总是拿新创建的第一个节点去思考怎样操作
    //如果思考如何操作第一个节点,只需要把节点地址赋值给头结点的指针域即可
    //然而这样思考,生成第二个节点怎么办,第三个,第四个呢?
    //你应该想在含有多个节点的链表中在头部插入数据怎么办。
}

 

2.尾插法:当然就是乖乖排到后面去啦,这才像话。时间复杂度O(n)
template <class T>
void LinkList<T>::tail_creat(T val)
{
   Node<T>* node = new Node<T>(val);
   Node<T>* tail = head;
   for(int i = 0; i < length; i++){
       tail = tail -> next;
   }
   tail -> next = node;
   length++;
}

 

3.中间插入
template <class T>
bool LinkList<T>::insert(int i, T val)
{
    Node<T> * ran = head;
    Node<T>* node = new Node<T>(val);
    //下标不合法
    if(i < 0 || i > length)return false;
    //遍历到n个节点
    for(int i = 0; i < length; i++)ran = ran -> next;
    //插入
    node -> next = ran -> next;
    ran -> next = node;
    length++;
    return true;
}

 


首先定位到这个要插入位置的前一个位置x,让要插入的节点的下一个节点是x的下一个节点,再把x的下一个节点改为要插入的节点。关于这个时间复杂度,网上众说纷纭。
有人认为计算时间复杂度时,我们只考虑原子操作,即不能再分割的关键操作,就是插入。时间复杂度为O(1),另一种看法是你要先定位到这个节点,然后插入时间复杂度是
O(n)。我还是同意后者。那么又有个问题,顺序表插入也是O(n),那为啥说链表比你顺序表插入快。其实,仔细分析就可以知道,顺序表的插入是先定位,再移动。数组这个东西,
存取快,时间复杂度是O(1),移动,实际上是寄存器大量的写操作。而单链表呢,首先是时间复杂度为O(n)的定位操作,这个是读操作,再进行O(1)的写,我们知道读操作比写操作快,
所以我们抓主要矛盾,就写操作而言,链表自然插入快得多。

下次回答链表时间复杂度这个问题,就要分情况说。

  删除

    

1.删除中间节点
  这个简单,遍历到要删除的节点的前一个节点x,改变x指针的指向即可,就是把x的next指针指向下一个节点的下一个节点。
template <class T>
bool LinkList<T>::del(int i)
{
    //遍历到i-1个节点
    int j = 1;
    Node<T>* p = head;
    while(j < i && p != nullptr){
        j++;
        p = p -> next;
    }
    //p == nullptr说明i大了
    //当遍历到最后一个节点的时候,这时候也是无法删除的,也就是说,p只能在第一个节点和倒数
    //第二个节点之间。这一点和插入不一样,你插入的话,可以遍历到最后一个就节点。可以把最后一个的节点的
    //下一个NULL节点也视为一个节点。而且先判断p是否为null,在判断p->next是否是null
    //如果先判断p -> next是否为null的话,p为null时出现段错误。也就是说,p不为nullptr时,才能使用p -> next
    if( p == nullptr || p -> next == nullptr || i <= 0 || j > i)return false;//j > i使得程序更健壮
    //删除节点
    Node<T>* des = p -> next;
    p -> next = des -> next;
    delete des;
    length--;
    return true;
}

 

 
2.删除第一个节点,引入头结点
  说实话,如果没有头结点的话,删除第一个节点和删除中间节点很难统一起来,因为你要遍历到要删除节点的前一个节点,第一个节点的前一个节点是啥,喔喔,是头结点。
所以,头结点用处大大的,这种思想叫哨兵思想。代码和上面差不多。
3.覆盖删除

   上次刷leetcode时,遇到这么一道题。

//删除一个链表中的节点(非尾节点),但是只给你要删除的节点指针。
//一开始,我还很纳闷儿,链表头结点都不告诉
//后面看了题解,这真是鬼才,他用后一个节点直接覆盖这个要删除的节点。这里说的覆盖是内存中的内容,和深拷贝有点像。
//不过要删除节点的后面那个节点内存可能不被释放,但我也不清楚链表内存分配是否是用new来动态分配,还是不要delete的好。
/*
* * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: void deleteNode(ListNode* node) { * node = *(node -> next); } };

 

  

  反转

.双指针法
    
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;//第一个节点没有前一个节点,初始化为null
        ListNode* cur = head;//当前节点
        ListNode* temp;//后一个节点
        while(cur != nullptr){
            temp = cur -> next;//因为后面要修改cur -> next,提前保存下来
            cur -> next = pre; 
            //下面两条语句顺序不能颠倒
            pre = cur;
            cur = temp;
        }
        return pre;//退出循环后,cur为null,pre为新链表的第一个节点
    }
};

 

 

  

3.头插法。头插法每次提交内存占用最低,然而最慢。谁能告诉我为啥?

 

template <class T>
void LinkList<T>::head_reverse(){
    //1.链表为空,不用反转
    if(head -> next == nullptr)return;
    //2.头插法反转
    Node<T>* vir_head = new Node<T>;
    Node<T>* ran = head -> next;
    Node<T>* p;
    while(ran != nullptr){
        p = ran -> next;
        ran -> next = vir_head -> next;
        vir_head -> next = ran;
        ran = p;
    }
    delete head;
    head = vir_head;
}

 

 

    

  

posted @ 2020-05-06 22:27  FizzPu  阅读(249)  评论(0编辑  收藏  举报