对接CSP-J/S认证C++算法蓝桥等考导学/四级:线性数据结构/之二:(15)双向链表之双向链表


一、观看PPT教程 

01】双向链表

二、练习题(不清楚回头查看有关PPT)

01】下面是有关双向链表的定义的描述,如果有错误,请改正:双向链表的每个节点包含两个指针域,一个指向前一个节点,一个指向后一个节点。
如果存在头节点,头指针head指向它,否则指向第一个节点。
如果存在头节点,那它的向前指针一般指向NULL,即不指向任何节点;向前指针指向第一个节点(非空链表)或NULL(空链表)。如果不存在头节点,那第一个节点充当头节点的功能,只不过数据域中有数据。尾节点的向后指针一般指向NULL,即不储存任何节点信息。
02】下面是使用三静态数组模拟双向链表(有头节点)的代码,请补全缺失的注释:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

#include<iostream>using namespace std;const int max_size = 1001; //int head; //int val[max_size];  //int pre[max_size];  //int nxt[max_size];  //int idx;  //int size;  ////初始化void init(){  head = 0;  //  pre[0] = -1, nxt[0] = -1; //  idx = 1;  //  size = 0;  //} //空链表:当链表中仅有头指针和头节点或仅有头指针时,此时链表为空 。//模拟指针从头节点开始查找节点int find(int pos) //传入pos {  if(pos > size || pos < 0)  //  {    cout << "查找位置不存在" << endl;    return -1;  }  else    {    int fd = head;  //    for(int i = 1; i <= pos; i++) //    {      fd = nxt[fd];    }    return fd;  //  }}/**双向链表的插入1. 头插法2. 中间插入法3. 尾插法插入位置的前后节点和新节点之间的指向关系需要重新构建。1.新节点的 pre 指向前一个节点;2.新节点的 nxt 指向后一个节点;3.前一个节点的 nxt 指向新节点;4.后一个节点的 pre 指向新节点。想一想,这4步的顺序哪些步骤不能改变?答:先建立新节点的指向关系,再建立它的前后节点指向关系。即1、2的步骤放在3、4之前。**//**插入第一个节点 双向链表初始为空时,由于头节点的前后指向为—1,为避免数组访问越界。特别地,当空链表插入第一个元素时,做出特殊处理,直接构建头节点和第一个节点的指向关系。**/ void push_first(int x)  //传入一个节点值{  val[idx] = x; //保存节点值  pre[idx] = head;  //向前指针指向头节点  nxt[idx] = -1;  //向后指针指向NULL  nxt[head] = idx;  //头节点向后指针指向第一个节点   idx++;  //新节点储存位置增加1  size++;  //链表长度增加1 } //头插法:将新节点插入到头部void push_front(int x)  //待插入的值x{  if(idx > max_size - 1)  //检查数组是否已满       cout << "储存位置已超过范围" << endl;  else if (size == 0){ //检查链表中是否还没有节点(空链表)      push_first(x);   //插入第一个节点       return;  }else    {    val[idx] = x;  //    pre[idx] = head;  //    nxt[idx] = nxt[head];  //    nxt[head] = idx; //    pre[nxt[idx]] = idx;  //    idx++;  //    size++;  //单链表长度增加1   } }//中间插入法:将新节点插入知道位置前。void insert(int pos, int x) //pos插入位置 {  if(pos > size || pos <= 0) //      cout << "插入位置不存在" << endl;  else if(idx > max_size - 1)      cout << "储存位置已超过范围" << endl;  else  {    val[idx] = x;  //    int temp = find(pos);  //    pre[idx] = pre[temp];  //    nxt[idx] = temp;  //    nxt[pre[temp]] = idx;  //    pre[temp] = idx;  //    idx++;  //    size++;  //单链表长度增加1   }  }//尾插法:将新节点插入末尾void push_back(int x) //传入待插入的值x {  if(idx > max_size -1)  //      cout << "储存位置已超过范围" << endl;  else if (size == 0) //检查链表中是否还没有节点(空链表)      return push_first(x);   //插入第一个节点   else    {    val[idx] = x; //    int temp = head;    while(nxt[temp] != -1) //        temp = nxt[temp];    pre[idx] = temp;    nxt[idx] = nxt[temp];  //      nxt[temp] = idx; //    idx++;  //    size++;  //单链表长度增加1  }}/**双向链表的删除删除节点,前节点直接指向后节点。1. 删除第一个节点  注:这里第一个节点不是头节点 2. 删除最后一个节点3. 删除中间的某个节点。注:数组模拟的链表很难物理移除数据,故模拟代码中不含数据移除算法,模拟删除过程不是实际的删除过程,节点删除后就成了垃圾。**///删除第一个节点void pop_front(){  if(size == 0)  //      cout << "链表为空" << endl;  else if (size == 1){  //    nxt[head] = -1;    size--; //链表长度减1  }else{ //    nxt[head] = nxt[nxt[head]];   //头节点的向后指针指向第二个节点     pre[nxt[head]] = head;   //原第二个节点的向前指针指向头节点     size--;  //链表长度减1  }}//删除最后一个节点void pop_back(){  if(size == 0)  //      cout << "链表为空" << endl;  else  //  {    int temp = head;  //从头指针开始查找    while(nxt[temp] != -1) //temp定位为尾节点        temp = nxt[temp];    nxt[pre[temp]] = -1; //    size--;  //链表长度减1  }  }//删除中间节点void erase(int pos) //传入删除位置 {  if(pos > size || pos <= 0)  //      cout << "删除位置不存在" << endl;  else if (pos == size)  //      pop_back();  else  //  {    int temp = find(pos);  //    nxt[pre[temp]] = nxt[temp];  //    pre[nxt[temp]] = pre[temp];  //    size--;  //链表长度减1  }}/**双向链表的修改 **/void modify(int pos, int x) //传入位置pos和新值x{  if(pos > size || pos <= 0)  //      cout << "修改位置不存在" << endl;  else      val[find(pos)] = x;  //  } /**双向链表的遍历(双向) **/void print_list(){  int temp = nxt[head]; //  int tail = temp;  //尾节点   while(temp != -1)  //  {    cout << val[temp] << "  ";  //    tail = temp;  //更新尾节点     temp = nxt[temp];  //沿向后指针前进1节点   }  cout << " | " ;  //从尾节点开始遍历   while(tail > head)   //不是头节点   {    cout << val[tail] << "  ";  //输出节点值     tail = pre[tail];  //沿向前指针前进1节点   }  cout << endl;}/**双向链表测试**/ int main(){  //初始化   init();    int tvs[] = {1,20,300,4000,50000,600000};    //push_front测试  for(int i = 0; i < 6; i++){    push_front(tvs[i]);    print_list();   }   //push_back测试  push_back(0);  print_list();  //insert测试  insert(4, 250);  print_list();  //find测试   int temp = find(4);  cout << "第4个节点的位置是" << temp << ",值是" << val[temp] << "。" << endl;  //modify测试  modify(4, 350);  print_list();   //erase测试  erase(4);  print_list();  //pop_back测试  pop_back();  print_list();   //pop_front测试  pop_front();  print_list();  return 0;}

03】循环双向链表与双向链表不同的地方是,头尾节点的指针的指针会互相指向,即头节点的前指针会指向尾节点,尾节点的后指针指向头节点,形成双向循环结构。把上面的程序改造成适用于循环双向链表的程序。

04】下面是双向链表的总结,如果有错误,请改正。

时间复杂度——删除操作:若只知道待删除节点的序号,则依然要按序查找,时间复杂度仍为O(n);插入操作:若给定前驱节点,则时间复杂度均为O(1)。否则只能按序或按值查找前驱节点,时间复杂度为 O(n);查找:时间复杂度均为O(n)。双向链表本身的结构优势在于,可以O(n)地找到前驱节点,若算法需要对待操作节点的前驱节点做处理,则双向链表相比单链表有更加便捷的优势。双向链表缺点在于浪费空间来存储指针域,(若从工程角度考量,则其维护性和可读性都更低),所以常规情况下,使用单链表的较多。05】编程题:双向约瑟夫环