| 这个作业属于哪个班级 | 数据结构-网络20 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业01--线性表 |
| 这个作业的目标 | 学习线性表相关内容 |
| 姓名 | 余智康 |

目录

0. 展示PTA总分

1. 本章学习总结

2. PTA实验作业

3. 阅读代码



0.PTA得分截图


1.本周学习总结(5分)

1.1 绪论(1分)

1.1.1 数据结构有哪些结构,各种结构解决什么问题?

  • 数据结构分为逻辑结构和存储(物理)结构:
  1. 逻辑结构:
  • 集合:集合中含有多种元素,这些元素无序存在。

  • 线性结构:(一对一),除了开始和最后的元素外,每一个元素都有一个前驱和一个后继的n个数据元素有序集合。
    可以使用线性结构完成名单之类有序信息的记录。

  • 树结构:(一对多),除开始元素,每一个元素都有一个前驱;除终端元素,每一个元素都有一个或多个后继。

  • 图结构:(多对多),每个元素的前驱个数和后继个数是任意的,所以可能没有开始、终端元素,也可能有多个开始、终端元素

2.存储结构:

  • 顺序存储结构:连续的存储单元存放所有元素

  • 链式存储结构:每个结点单独申请,所占的存储空间一定连续,需要给每个结点添加指针域,存放相邻结点的地址

1.1.2 时间复杂度及空间复杂度概念。

  1. 时间复杂度用于衡量程序执行的时间,它取执行频度T(n)的数量级。若程序执行的次数与问题规模(n)无关,则时间频度为O(1);
  2. 空间复杂度是算法在运行过程中临时占用的储存空间的量度。若临时占用的空间对于问题规模来说是常数,那么该算法就称为就地工作算法或原地工作算法。

1.1.3 时间复杂度有哪些?如何计算程序的时间复杂度和空间复杂度,举例说明。

  1. 时间复杂度:
    从小到大排序为:O(1) < O(log2 n) < O(n) < O(n log2 n) < O(n^2) < O(n^3) < O(2^n) < O(n!)
  • 计算:通过分析执行的频度,取数量级来表示
  • 简化分析:仅仅考虑算法中的基本操作,(基本操作:是多重循环中的最深层语句)
    void func(int n)
    {
          int i = 0, s = 0;
          while( s < n)
          {
                i++;        //基本操作
                s = s + i;  //基本操作
          }
    }

设循环执行k次,变量i从0递增1,直到k为止。
所以循环结束时,s = (1 + k) * k * (1/2)>= n,增加一个用于修正的常数C,则(1 + k) * k * (1/2) + c = n
解得,k = [-1 + √(8n + 1 -8c)] * (1/2), 从而得出该算法的时间复杂度为:O(√n)

  • 求和、求积的计算:设T1(n) = O(f(n)), T2(n) = O(g(n))
  • 求和:T1(n) + T2(n) = O(MAX(f(n),g(n)))
  • 求积:T1(n) * T2(n) = O(f(n) * g(n))
    for(i = 0; i < n; i++)      /*片段1*/
    {
        printf("i = %d  ",i);
    }
                                /*片段2*/
    for(i=0; i<n; i++)          // 循环1
        for(j=i; j>=1; j/=2)
            printf(“%d\n”, j);  // 循环2

片段1中,易得,T1(n) = n;
片段2中,
不考虑循环1时,设循环2执行k次,得出 n / 2 ^ k + c = 0, 解出T2(n) = O(log2 n)
而循环1执行n次,得出T3 = O(n),
由此得出,片段2中, 时间复杂度为 O(n * log2 n)

由于片段1的时间复杂度小于片段2的时间复杂度,故该算法的时间复杂度为 O(n log2 n)

  • 递归:先写出递推式,然后求解递推式得出算法的执行时间,从而得出时间复杂度
  int fact(int n)

  {     
      if(n<=1)
          return 1;   
      else     
          return n*fact(n-1); 
   } 


当 k = 1时, T(k) = 1,
当 k ≠ 1时, T(k) = T(k-1) + 1 = T(k-2) + 1 + 1 = T(1) + 1 + ... + 1 = n

  1. 空间复杂度
  • 计算:通过分析函数体中所占用的临时空间,取数量级来表示
  void fun(int a[], int n, int k)
  {
      int i;
      if( k == n-1)
      {
           for(i = 0; i<n; i++)
           {
                printf("%d\n",a[i]);
           }
      }
      else
      {
            for(i = k; i<n; i++)
                 a[i] = a[i] + i * i;
            fun(a, n, k+1);
      }
  }

分析fun(a, n, 0)时的空间复杂度, 设fun(a, n, k)所占的临时空间为S1(n, k)
当k = n - 1时, 仅定义了一个变量,S(n) = O(1);
当k ≠ n - 1时, S1(n, k) = S1(n, k+1)
可得:S(n) = S1(n, 0) = 1 + S(n,1) = 1 + 1 + ... + 1 + S1(n,n-1) = n = O(n)


1.2 线性表(1分)

1.2.1 顺序表

介绍顺序表结构体定义、顺序表插入、删除的代码操作
介绍顺序表插入、删除操作时间复杂度

  1. 结构体定义:
    * 思路:需要数组,需要长度
    * 代码:
typedef struct SqList
{
    int data[MAXSIZE];
    int length;
}SqList,*List;
  1. 顺序表插入
  • 思路:挪动数组,长度增加

  • 挪动,

  • 将insert插入,

  • 将length增加

  • 时间复杂度:O(n)

  • 代码:

//  position 为插入的位置,
//  num_insert 为插入的数据,

  for( i = L->length; i > position; i--)    //挪动数组
  {
        L->data[i] = L->data[i=1];
  }
  L->data[position] = num_insert;   //插入
  L->length++;    //长度增加 
  1. 顺序表删除
  • 思路:挪动数组,长度减少
  • 挪动,以删除data2为例
  • 将length减小
  • 时间复杂度: O(n),
  • 代码:
//  position 为删除的位置,

    for(i = position; i < L->length - 1; i++)
    {
          L->data[i] = L->data[i+1];
    }
    L->length--;  //长度减少

1.2.2 链表(2分)

画一条链表,并在此结构上介绍如何设计链表结构体、头插法、尾插法、链表插入、删除操作
重构链表如何操作?链表操作注意事项请罗列。

  1. 链表:
    *
    * 结构体:思路:需要数据、以及指针域
typedef struct LNode
{
    int data;
    srtuct LNode * next;
}LNode, *LinkList;
  1. 头插法:
  • 新建头结点,并将它的next指向空:
  • 新建结点,先把head的next赋值给新结点的next,再将新结点的地址赋值给head的next
    node -> next = L->next;
    L->next = node;
  1. 尾插法:
  • 新建头结点,并它的next指向空:

  • 新建尾指针,指向最后一个结点:

  • 新建结点,将新结点的地址赋给tail的结点的next,在将tail指向tail指向的结点的下一个结点(即,最后一个结点)

  • 最后让tail指向的结点的next指向NULL

    while()
    {
        ...
        tail -> next = node;
        tail = tail ->next;
    }
    tail ->next = NULL;
  1. 链表插入:
  • 思路:通过前驱,修改next关系
  • 代码:
    LinkList node;      //新建结点,并输入数据
    node = new LNode;
    cin >> node -> data;

    node -> next = pre -> next;  //修改next关系
    pre -> next = node;
  1. 删除:
  • 思路:找前驱,修改next关系

  • 代码:

    LinkList p;
    p = pre -> next;    //暂时保存要删除元素的地址
    
    pre -> next = pre ->next ->next;  //修改next关系
    delete p;    //释放要删除元素的空间
  1. 链表:
    * 存储空间:1)不连续的空间,需要多少申请多少,提高了空间的利用率
    * 插入: 1)不需要挪动数据,仅修改next关系,若是已知要删除元素的前驱,时间复杂度为O(1)
    * 删除: 1)不需要挪动数据,并物理意义上删除了数据,若是已知要删除元素的前驱,时间复杂度为O(1)

  2. 顺序表:
    * 存储空间:1)连续的空间,但需要申请的空间一般大于所使用的空间,可能对空间有所浪费;2)空间有限,若是后续要添加的数据过多,可能溢出
    * 插入: 1)需要挪动数据;2)插入的数据过多时,数组可能溢出
    * 删除: 1)需要挪动数据;2)并没有真正删除了数据

  3. 链表操作注意事项:
    * 链表的非空判断;
    * 对链表结点的next关系进行修改之前,一般注意保留后继结点;
    * 链表有无头结点
    * 遍历时要记住新建遍历指针,一般不要将指向头结点的头指针拿去当遍历指针(犯过这种错误、(o_ _)ノ);
    * 链表的类型:单链表,循环单链表,循环双链表等;
    * 删除结点后,记得delete;

1.2.3 有序表及单循环链表(1分)

有序顺序表插入操作,代码或伪代码介绍。
有序单链表数据插入、删除。代码或伪代码介绍。
有序链表合并,代码或伪代码。尽量结合图形介绍。
有序链表合并和有序顺序表合并,有哪些优势?
单循环链表特点,循环遍历方式?

  1. 有序顺序表插入(以递增为例):
  • 代码介绍:
    // insert_num  为插入的数据
    int position = 0;    // 存放要插入的位置

    for(i = L->length - 1; i >= 0 && L->data[i] < insert_num; i--)
    {
         L->data[i + 1] = L->data[i];
    }
    position = i+1;    //找位置

    L->data[position] = insert_num;    //插入
    L->length++;    //增加长度
  1. 有序单链表插入、删除(以递增为例):
  • 代码介绍:

    LinkList pre = L;
    while(pre->next && pre -> next > insert_num )  //遍历,寻找插入位置的前驱
    {
         pre = pre->next;
    }
    
    LinkList node = new LNode;    //新建结点,存放插入数据
    node->data = insert_num;
    
    node->next = pre->next;     //修改next关系,完成插入
    pre->next = node;
  1. 有序链表合并:
  • 代码介绍:
     LinkList pL1 = L1->next;    //新建遍历指针
     LinkList pL2 = L2->next;

     LinkList merge_L = L;      //重构链表
     L->next = NULL;

     while(pL1 && pL2)          //任一为空时结束循环
     {
          if(pL1->data == pL2->data)  //相同
          {
               merge_L -> next = pL1;
               merge_L = merge->next;

               pL1 = pL1 ->next;
               pL2 = pL2 ->next;
          }
          else if(pL1->data < pL2->data)  
          {
               merge_L -> next = pL1;
               merge_L = merge->next;
                
              pL1 = pL1 ->next;
          }
          else 
          {
               merge_L -> next = pL2;
               merge_L = merge->next;

               pL2 = pL3->next;
          }
     }

      if(pL1)    //若pL1所指向为空,则pL2剩下的数据元素接到merge_L的后面
          merge_L ->next = pL2;
      else
          merge_L ->next = pL1;
  1. 有序链表合并的优势:
  • (空间上)有序顺序表合并需要新建一个数组;有序链表可以在原链表上进行重构,更节省空间
  • (时间上)若合并的某个表先为空,有序顺序表需要将剩下的元素遍历存入重构数组中;而有序链表仅仅修改next关系即可
  1. 单循环链表特点,循环遍历方式:
  • 特点:
    1)尾结点的next不指向NULL,而是指向头结点
    2)可以从任何位置开始遍历整个链表
  • 遍历条件:
    LinkList p = L->next;  
    while(p != L)    //与单链表不同的是,将 p!= NULL 改为了p!= L
    {
         ...
         p = p->next;
    }


2.PTA实验作业(4分)

此处请放置下面2题代码所在码云地址(markdown插入代码所在的链接)。

2.1 两个有序序列的中位数

2.1.1 解题思路及伪代码

  • 解题思路:
  1. 已知个数、且递增 ——> 通过简单计算得出中位数的位置
  2. 遍历,同时计数,当负责计数的变量值等于中位数的位置时,找到中位数
  • 所需函数:链表的创建与销毁、找中位数
  • 伪代码(找中位数):
LinkList MidLNode(LinkList L1, LinkList L2, int position_mid)
{
    if mid_position = 0
        return NULL;  //表示该表为空表
    end if
      
    LinkList pL1 = L1->next;
    LinkList pL2 = L2->next;
    LinkList pMid;    //指向中位数
    int count == 0;

    while count != mid_position  do    //未到达中位数位置时,循环继续
        if pL1->data <= pL2->data
              mid = pL1;
              pL1 = pL1->next;
        end if
        
        else
              mid = pL2;
              pL2 = pL2->next;
        end else
    
        count++;
    end while    

    return mid;
}

2.1.2 总结解题所用的知识点

  • 链表遍历、找到指定的第几个位置的数据结点

2.2 一元多项式的乘法与加法运算

2.2.1 解题思路及伪代码

  • 解题思路:
  1. 多项式加法:链表的合并。可以重构链表L1,将加法的结果放在链表L1中,以便乘法时使用
  2. 多项式乘法:多个链表的合并。L1的每一项和L2相乘,再相加。
    • 可以将L1的一项和L2相乘的结果放在temp_L链表中,
    • 调用合并的函数L_multiply 和temp_L合并,结果放在L_multiply链中
  3. 特殊情况处理:
    • 结果计算完后,通过自定义的函数DelEmpty(),删去系数为0的项
    • 输出时,若是链表为空,输出 0 0
  • 所用函数:
    • 链表的创建和销毁、输出、多项式合并、多项式相乘、删去系数为0的项、
  • 伪代码:
// 多项式合并:
LinkList MergeList(LinkList L1, LinkList L2)
{
        pL1 = L1 ->next;
        pL2 = L2 -> next; 
        tail = L1;        

        L_merge = L1;
        L_merge->next = NULL;
        
        while(pL1 != NULL && pL2 != NULL)  do
        {
            if( pL1 -> index = pL2 ->index)  //如果指数相等
            {
                   pL1->coefficient = pL1->coefficient + pL2->coefficient;//系数相加,存放在pL1中
                   再将pL1所指的结点接到tail后面;
                   tail = tail->tail;    //tail移动
                   pL1、pL2移动;
             }end if
              
             else if (pL1 ->index > PL2 ->index) 
             {
                   将pL1所指的结点接到tail后面;
                   tail = tail->tail;    //tail移动
                   pL1移动;
             } end else if

           //...pL2所指结点的指数大时,处理情况与pL1大类似,不多赘述
        }end while
      
        if(pL1)  //如果while循环结束,pL1仍剩余
            tail ->next = pL1;
        end if
        else 
             tail ->next = pL2;
        end else

        return L_merge;  //返回头结点
}

// 多项式相乘:
LinkList MultiplyList(LinkList L1, LinkList L2)
{
     新建LinkList指针L_multiply(头结点)存放最终相乘后的多项式
     新建LinkList指针L_temp,存放计算过程中多项式

      while(pL1)
      {
            L_temp->NULL;      //每轮开始时,重构L_temp;
            tail = L_temp;
            pL2 = L2->next;    //每轮初始化pL2
  
            while(pL2)
            {
                  新建node结点,并分配内存;
                //node的介绍:node结点存放pL1所指的数据与pL2所指的数据相乘;
  
                  node->index = pL1->index + pL2->index;
	          node->coefficient = pL1->coefficient * pL2->coefficient;
              
                  将node接到tail后面;
                  tail、pL2移动到后一个结点
            }
            tail -> next = NULL;  //尾结点的next为空
            将pL1移动到后一位  
      
            调用函数MergeList(),将L_multiply和L_temp合并,
            合并后的多项式在L_multiply中
      }end while
       return L_multiply;
}end while

2.2.2 总结解题所用的知识点

  • 多项式合并,用到了链表合并的知识点:
    • 链表重构,尾插法、链表遍历
  • 多项式相乘,用到了尾插法新建链表、调用函数


3.阅读代码(1分)

3.1 题目及解题代码

  • 题目描述

判断给定的链表中是否有环。如果有环则返回true,否则返回false。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(slow!=NULL&&fast->next!=NULL&&fast->next->next!=NULL)
        {
            fast = fast->next->next;
            slow = slow->next;
            if(fast==slow)
            {
                return true;
            }
        }
        return false;
    }
};

3.2 该题的设计思路

设计思路:

  • 使用快慢指针,指针p1每次移动一个结点,指针p2每次移动2个结点,若p1、p2相遇则证明环存在
  • 算法的时间复杂度:算法中涉及一个循环,时间复制度为O(n)
  • 空间复杂度:临时变量占用的临时存储空间与问题规模无关,空间复制度为O(1)

3.3 分析该题目解题优势及难点。

  • 题解优势:使用了快指针和慢指针解决了分析链表是否有环的问题。
  • 延申:在查找倒数第k个数据元素时也可以使用类似的两个指针遍历的方式:一个指针1先走k个位置,之后指针2再和指针1一起开始移动,当指针1为空时,指针2所指的数据元素就是倒数第k个元素。
  • 难点:是否考虑到快慢指针的使用