02_线性存储结构小结

02_线性存储结构小结

  • 线性表是存储数据中具有一对一关系的一种存储结构。
  • 注意这里的线性表指的是存储结构(顺序表、链表),而不是存取结构(栈、队列等)。存取结构也可以基于存储结构来实现,比如顺序栈链栈
  • 线性存储结构共同点是将数据按照顺序用一根线串联起来。
  • 所存储的数据类型必须相同。
    • 对于顺序表而言。每一个结点元素的类型都是相同的,因为需要通过索引来找到元素的位置,则每个结点元素的长度必须相同,符合相同结构体定义的数据才能被线性表的统一规则正确访问。
    • 对于链表而言。链表对结点的访问依赖于指针而不依赖于索引,即是不依赖于结点的相同性。但归根结底,链表也是数据存储结构,对结点的访问终究是要访问到具体的数据。两个结点如果属于不同的结构体,那么通过结构体变量访问数据项的时候,不同的数据类型所占的长度也是不一样的,无法通过统一规则访问。所以链表也要求数据类型必须相同。

1 顺序表

  • 通过元素的顺序存储来反映数据间的逻辑关系。

  • 可以通过索引随机存取顺序表的元素。

    • 索引访问的优势。定义顺序存储结构时,结点首地址结点大小是已知的,借助于地址空间的连续性,拿到索引(偏移量)就可以精确计算出访问元素的位置。因此更适合查询更新元素多的场景。时间复杂度O(1)

    • 索引访问的不足。所有的插入删除操作需要通过移动元素来实现,因为需要保证存储结构所定义的地址空间连续性,元素通过索引访问的正确性。移动元素是一个非常耗性能的操作。若顺序的长度为n,那么平均时间复杂度O(n/2)。

  • 存储结构定义。

    • 由于顺序表是顺序存储,且通过索引访问元素,则用一个数组来存储它的元素。
    • 涉及到插入元素可能超过数组容量,所以需要实现动态数组的功能。
      • 指针来存储数组首地址
      • 用一个length字段来存储当前元素个数
      • 动态数组最大容量,是判断是否需要扩容的依据。
      • 达到扩容条件时候,每次扩容需要增量
    // 定义初始大小和增量
    #define LIST_INIT_SIZE 150
    #define LIST_INCREMENT 20
    // 定义返回状态及返回码
    #define Status int
    #define OK 1
    #define ERROR 0
    // 定义存储的元素类型
    #define ElementType int
    
    // 顺序表的存储结构定义
    typedef struct
    {
    	// 线性表存储空间地址
    	ElementType* elem;
    
    	// 线性表当前存放元素的个数
    	int length;
    
    	// 当前线性表的容量
    	int listSize;
    
    	// 约定每次扩容的增量
    	int incrementSize;
    }SqList;
    
  • 常用操作注意项。

    • 初始化 -> 分配内存需要注意失败的情况。

      /* 01_顺序表——创建空表:没有元素,长度为0,默认容量和增量*/
      Status createList_Sq(SqList& sL, int initSize = LIST_INIT_SIZE, int incrementSize = LIST_INCREMENT)
      {
      	// 开辟顺序表存储空间
      	sL.elem = (ElementType*)malloc(initSize * sizeof(ElementType));
      	// 创建失败,返回错误码
      	if (!sL.elem)
      	{
      		return ERROR;
      	}
      	sL.length = 0;
      	sL.listSize = initSize;
      	sL.incrementSize = incrementSize;
      	return OK;
      }
      
    • 插入元素

      /* 05_顺序表——插入元素:在指定索引 index 之前插入一个元素*/
      Status insertElem_Sq(SqList& sL, int index, ElementType elem)
      {
      	// 索引不合法
      	if (index < 0)
      	{
      		return ERROR;
      	}
      	// 是否进行扩容
      	if (sL.length + 1 >= sL.listSize)
      	{
      		sL.elem = (ElementType*)malloc((sL.listSize + (size_t)sL.incrementSize) * sizeof(ElementType));
      		// 扩容失败
      		if (!sL.elem)
      		{
      			return ERROR;
      		}
      		sL.listSize += sL.incrementSize;
      	}
      	// 在末尾添加
      	if (index >= sL.length)
      	{
      		//sL.elem[sL.length] = elem;
      		*(sL.elem + sL.length) = elem;
      	}
      	else
      	{
      		// 插入前从后往前移动元素,注意索引取值
      		/*for (int i = sL.length - 1; i >= index; i--)
      		{
      			sL.elem[i + 1] = sL.elem[i];
      		}*/
      		for (ElementType* p = sL.elem + sL.length + 1; p >= sL.elem + index; p--)
      		{
      			*(p + 1) = *p;
      		}
      		//sL.elem[index] = elem;
      		*(sL.elem + index) = elem;
      	}
      	// 不要忘记长度
      	++sL.length;
      
      	return OK;
      }
      
      • 插入索引在当前长度范围外的定义。
      • 注意判断是否达到扩容条件,以及扩容失败后的操作。
      • 插入成功后不要忘记当前元素长度字段。
    • 删除元素 -> 同插入。只是少了扩容条件,多了返回元素的功能性定义。

      /*  08_顺序表——删除:删除指定索引的元素*/
      ElementType deleteElem_Sq(SqList& sL, int index)
      {
      	// 如果不在元素索引范围内
      	if (index < 0 || index >= sL.length)
      	{
      		return -1;
      	}
      	
      	ElementType toDelete = *(sL.elem + index);
      	// 数组方式
      	/*for (int i = index; i < sL.length - 1; i++)
      	{
      		sL.elem[i] = sL.elem[i + 1];
      	}*/
      	// 指针方式
      	ElementType* p = sL.elem + index;
      	ElementType* q = sL.elem + sL.length - 1;
      	while (p < q)
      	{
      		*p = *(p + 1);
      		p++;
      	}
      	// 别忘记长度减一
      	sL.length--;
      	return toDelete;
      }
      
    • 清空和销毁。

      • 清空是置为初始化状态,存储结构定义还在。

        /* 06_顺序表——清空*/
        Status clearList_Sq(SqList& sL)
        {
        	// 先清空数组中的值
        	delete[] sL.elem;
        	// 重新分配
        	sL.elem = (ElementType*)malloc(LIST_INIT_SIZE * sizeof(ElementType));
        	if (!sL.elem)
        	{
        		return ERROR;
        	}
        	// 其余长度置为初始
        	sL.length = 0;
        	sL.listSize = LIST_INIT_SIZE;
        	sL.incrementSize = LIST_INCREMENT;
        	return OK;
        }
        
      • 销毁需要释放空间。

        /* 07_顺序表——销毁*/
        Status destroyList_Sq(SqList& sL)
        {
        	// 回收数组空间
        	delete[] sL.elem;
        	//free(sL.elem);
        	// 其余控制元素置为0
        	sL.length = 0;
        	sL.listSize = 0;
        	sL.incrementSize = 0;
        	return OK;
        }
        

2 链表

  • 通过链式存储来反映元素之间的一对一关系。

  • 通过指针构成的链遍历链表的元素。

    • 链式存储的优势。对于插入和删除操作,只需要针对插入/删除元素与其相邻的元素进行操作,不存在大量移动元素的耗性能的情景。

    • 链式存储的不足。对于更新查询操作,需要从表头指针开始向后访问,即是以遍历的方式进行顺序访问,访问不方便。

主要通过单链表来记录链式存储结构的定义和常用操作,再此基础上类比循环链表和双向链表。

2.1 单链表

  • 存储结构定义

    • 对于数据而言。数据是零散存储的,每个元素结点只需要维护该结点的元素。

    • 对于数据间的一对一关系,需要指针进行维护。所以需定义指向当前元素结点类型的指针,完成对链的定义。

    • 对于链表结构,初始访问地址需要一个指针,拿到它才能对该链表进行操作。这里将这个指针的存储位置不同,将单链表分为带头结点和不带头结点。

      • 带头结点的单链表。即是以下结构体后的HNode类型。
        • 数据域的ElementType一般不用,或是维护该链表信息用,比如当前元素个数等。
        • 指针域 next指向真正存储的第一个结点元素,即首元结点。
      • 不带头节点的单链表。即是以下结构中的*SL,指向的元素即为第一个元素(首元)。
    // 定义返回状态及返回码
    #define Status int
    #define OK 1
    #define ERROR 0
    // 定义存储的元素类型
    #define ElementType int
    
    /* 定义单链表结点*/
    typedef struct SL_Node
    {
    	// 存储的元素
    	ElementType data;
    	// 指向下一个结点的指针
    	struct SL_Node* next;
    }HNode, *SL;
    

常用操作注意项

2.1.1 初始化操作
  • 带头结点。即是需要构造出一个头节点,并且返回或初始化该头节点的地址作为头指针。有两种方式创建头节点。

    • 通过指针的创建。由以上定义,SL指向该类型结点的指针。此处head即是指向头结点类型的指针。由于指针在定义的时候,我们是不知道该头指针的地址的,当它分配完内存后我们才能得知地址,涉及的操作需要修改指向头结点的head指针的值,我们必须将头指针作为引用类型传入,是得修改能对该变量生效。

      /* 01_单链表——初始化_带头结点
      */
      Status initList_SLh(SL& head)
      {
      	// 指向头节点的指针
      	head = (SL)malloc(sizeof(SL_Node));
      	if (!head)
      	{
      		return ERROR;
      	}
      	head->data = NULL;
      	head->next = NULL;
      	return OK;
      }
      
    • 通过指向头节点的二级指针创建。通过以上指针方式创建的讲述,应该会更好理解指向结点的二级指针方式创建头结点。

      /* 18_单链表——初始化_带头结点2*/
      Status initList_SLh2(HNode** hNode)
      {
      	*hNode = (HNode*)malloc(sizeof(SL_Node));
      	if (!*hNode)
      	{
      		return ERROR;
      	}
      	(*hNode)->data = NULL;
      	(*hNode)->next = NULL;
      	return OK;
      }
      
    • 同顺序表,两种方式内存分配都需要考虑分配失败的情况。

  • 不带头结点。即是需要初始化一个指向首元的指针,初始化时候没有元素,所以指针给一个NULL值即可。

    /* 02_单链表——初始化_无头结点*/
    Status initList_SL(SL& sL)
    {
    	sL = NULL;
    	return OK;
    }
    
2.1.2 查询、更新、求长度、遍历操作

需要从第一个元素依次往后查找。区分带头结点和不带头节点,这里举出求长度的操作。

  • 带头结点。头指针指向的是头节点,首元是头节点的next域指向的结点。

    /* 03_单链表——长度_带头结点*/
    int listLength_SLh(SL head)
    {
    	// 初始迭代长度
    	int length = 0;
    	// 指向头结点的变量
    	SL p = head->next;
    	while (p)
    	{
    		length++;
    		p = p->next;
    	}
    	return length;
    }
    
  • 不带头节点。头指针指向的即是首元结点。

    /* 04_单链表——长度_无头结点*/
    int listLength_SL(SL sL)
    {
    	int length = 0;
    	// 从头指针开始迭代
    	SL p = sL;
    	while (p)
    	{
    		length++;
    		p = p->next;
    	}
    	return length;
    }
    
2.1.3 插入元素方式

这里采用批量插入方式作为说明,对应的单个元素插入方式即为后插法、前插法。

同样,插入元素需要判断内存分配结果。

1 尾插法

这里是用于批量建立的过程。

整个过程只需要两个指针(待插入元素的指针s;其插入后的前驱位置元素指针p)

  • 类比为单个元素的后插法,与前插法比较,后插法不需要额外的指针。两个操作的顺序不可逆,可画图理解。

    // 1. 将【待插入元素】指向【前驱元素】的后继
    s->next = p -> next;
    // 2. 将【前驱元素】指向【待插入元素】
    p->next = s;
    
  • 带头结点

    /* 07_单链表——尾插法建立_带头节点*/
    Status tailInsert_SLh(SL& head, ElementType *datas, int length)
    {
    	// 指针p存放上一个结点
    	SL p = head;
    	// 用于新建的结点
    	SL s = NULL;
    
    	for (int i = 0; i < length; i++)
    	{
    		// 指针p,每次循环在最后一个结点指针域,进行结点创建
    		s = (SL)malloc(sizeof(SL_Node));
    		if (!s)
    		{
    			return ERROR;
    		}
    		s->data = *(datas + i);
    		s->next = NULL;
    		// 新结点连接到上一个结点
    		p->next = s;
    		// 迭代
    		p = s;
    		s = s->next;
    	}
    	return OK;
    }
    
  • 不带头结点

    • 对于头指针的方法,需要判断是否为第一个元素。
      • 因为插入元素需要保存上一个结点,不带头结点的链表,头指针无上一个结点,所以需要单独处理。
    /* 08_单链表——尾插法建立_不带头节点*/
    Status tailInsert_SL(SL& sL, ElementType* datas, int length)
    {
    	// 指针p,用来保存上一个结点
    	SL p = sL;
    	// 指针s,用于创建新结点
    	SL s = NULL;
    
    	for (int i = 0; i < length; i++)
    	{
    		// 指针p,每次循环在最后一个结点指针域,进行结点创建
    		s = (SL)malloc(sizeof(SL_Node));
    		if (!s)
    		{
    			return ERROR;
    		}
    		s->data = *(datas + i);
    		s->next = NULL;
    		// 不为第一个结点时
    		if (i)
    		{
    			// 将新节点连到p后面
    			p->next = s;
    			
    		}
    		// 为第一个结点时s新创建的地址赋值给链表标识sL,以及上一个结点标识p
    		else
    		{
    			sL = s;
    		}
    		// 新节点作为上一个结点
    		p = s;
    		// 指针q,再移动到新结点创建位置
    		s = s->next;
    	}
    	return OK;
    }
    
2 头插法

同样,这里是用于批量建立过程。

  • 下面两个方法是批量插入的方法,每一个插入的节点都是在头结点之后,即是head -> next,所以不需要额外的只针来保存该节点。如果是单个元素,则插入到指定位置前,需要从头找到前置节点。

  • 将该方法类比为单个元素的前插法。

    • 除了提供的插入元素指针s和插入位置的指针p,p向前找不能找到前置元素,需要从头找到p结点元素的前置元素。
    • 前插操作的两个语句可逆,因为三个结点都有指针进行保存。
    // 1. 找到p结点元素的前置结点
    // 头指针,指向头结点
    SL head;
    // 注意这里带头结点,从头结点开始;如果没有头结点,p为第一个元素的时候,需要特殊处理。
    SL q = head;
    while(q->next != p)
    {
    	q = q->next;
    }
    // 2. 找到q后即开始前插操作。两个语句顺序可逆。
    s->next = p;
    q->next = s;
    
  • 带头结点

    /* 09_单链表——头插法建立_带头结点*/
    Status headInsert_SLh(SL& head, ElementType* datas, int length)
    {
    	// 指针p,用作保存上一个创建的结点,使得头节点指向的新创建结点指向上一个结点。
    	SL p = head;
    	// 指针s,用于创建新的结点
    	SL s = NULL;
    
    	for (int i = 0; i < length; i++)
    	{
    		// 每次通过头结点创建新结点
    		s = (SL)malloc(sizeof(SL_Node));
    		if (!s)
    		{
    			return ERROR;
    		}
    		s->data = *(datas + i);
    		// 元素接到头结点后
    		head->next = s;
    		// 第一个结点插入
    		if (!i)
    		{
    			// 第一个结点即为表尾结点
    			s->next = NULL;
    		}
    		// 非第一个结点
    		else
    		{
    			// 结点前置为头结点
    			head->next = s;
    			// 结点后继为上一个结点
    			s->next = p;
    		}
    		// 指针p保存当次插入的结点位置
    		p = s;
    	}
    	return OK;
    }
    
  • 不带头节点

    /* 10_单链表——头插法建立_不带头结点*/
    Status headInsert_SL(SL& sL, ElementType* datas, int length)
    {
    	// 同上,指针p用来保存上一个创建的结点
    	SL p = sL;
    	// 指针s,用来创建新的节点
    	SL s = NULL;
    
    	for (int i = 0; i < length; i++)
    	{
    		s = (SL)malloc(sizeof(SL_Node));
    		if (!s)
    		{
    			return ERROR;
    		}
    		s->data = *(datas + i);
    		// 第一个结点
    		if (!i)
    		{
    			// 第一个结点作为表尾结点
    			s->next = NULL;
    		}
    		else
    		{
    			// 连接到上一次插入的结点
    			s->next = p;
    		}
    		// 连接到开始
    		sL = s;
    		// 保存为上一个结点
    		p = s;
    	}
    	return OK;
    }
    
2.1.4 删除元素

删除指定值的元素并返回。每次必须通过前驱结点的next判断该结点,上前驱结点才好更改指针指向。

  • 带头节点 -> 带头结点的好处是,不用额外考虑元素个数。

    • 多一个头结点就刚好满足了每一个待判断的结点都有前驱,首元结点的前驱即是头结点。
    • 若链表没有元素,则不用进入循环体,没有额外的考虑。
    /* 14_单链表——删除结点_带头结点*/
    void deleteElem_SLh(SL& head, ElementType data)
    {
    	// 保存待删除结点的上一个结点
    	SL p = head;
    	// 保存待删除结点
    	SL q = NULL;
    	while (p->next)
    	{
    		// 指针p总是在上一个结点的时候判断出下一个结点的条件
    		if (p->next->data == data)
    		{
    			q = p->next;
    			// 指针q若无后继结点,不影响
    			p->next = q->next;
    			free(q);
    			return;
    		}
    		// 否则继续查找
    		p = p->next;
    	}
    }
    
  • 不带头节点

    • 不太方便。
      • 第一个元素没有前驱结点,需单独判断。
      • 但是否有第一个元素还需要判断。带头结点的删除则可以直接进入循环体中判断。
    /* 15_单链表——删除结点_不带头结点*/
    void deleteElem_SL(SL& sL, ElementType data)
    {
    	// 指针p,保存待删除结点的前一个结点
    	SL p = sL;
    	// 指针q,指向p的后继结点
    	SL q = p->next;
    	// 如果链表没有结点,直接返回
    	if (!p)
    	{
    		return;
    	}
    	// 如果第一个结点就是待删除结点
    	else if (p && p->data == data)
    	{
    		sL = q;
    		free(p);
    		return;
    	}
    	// 两个以上结点
    	while (p->next)
    	{
    		// 指针p总是在上一个结点的时候判断出下一个结点的条件
    		if (p->next->data == data)
    		{
    			q = p->next;
    			p->next = q->next;
    			free(q);
    			return;
    		}
    		p = p->next;
    	}
    }
    
2.1.5 原地逆置单链表

『单链表原地逆置』个人理解。

  1. 大思路是将所有结点分为两种状态,『已完成逆置』和『待逆置』;
  2. 刚开始的状态,所有结点都是待逆置状态。
  3. 除了首元结点外,后续结点在执行逆置过程中都有结点处于已完成逆置状态。
  4. 为了方便理解,且不将『首元结点的逆置』与『其余结点逆置』过程作为不同子过程单独处理,需在首元结点逆置过程中『虚拟出一个空结点』作为『已完成逆置状态』的结点。
  5. 此次虚拟空结点作为『已完成逆置状态结点』,更方便将循环体划分出来。
  6. 难点在于划分出单轮次的循环过程。
  • 带头结点

    /* 16_单链表——原地逆置_带头节点*/
    void invertList_SLh(SL& head)
    {
    	// 指针p保存下一个带改变指向的元素,保证断开后头结点的指针能找到
    	SL p = head->next;
    	// 用指针p保存了,此处一定要置空,作为虚拟出的空结点
    	head->next = NULL;
    	// 指针s是要改变指向的元素
    	SL s = NULL;
    	// 特别注意理解第一轮的过程,虚拟一个NULL结点在头结点和首元结点之间,作为【已完成逆置】的元素。
    	while (p)
    	{
    		// 当前【待逆置的元素】完成,设置【下一个待逆置元素】为【当前待逆置】;
    		s = p;
    		// 指针p作为【下一个待逆置】
    		p = p->next;
    		// 当前待逆置元素,指向上一个逆置完成的元素(第一个结点的上一个元素即是虚拟出的空结点,此处在进入循环前置空)
    		s->next = head->next;
    		// 当前元素逆置完成,【头结点指针指向的元素】则是【上一轮逆置完成的元素】
    		head->next = s;
    	}
    }
    
  • 不带头结点

    • 实现上与带头结点的没有不同,仅仅头指针与头结点next域的替换。
    /* 17_单链表——原地逆置_不带头节点*/
    /* 过程同上,原理无不同,只有head->next替换为sL头指针*/
    void invertList_SL(SL& sL)
    {
    	SL p = sL;
    	sL = NULL;
    	SL s = NULL;
    	while (p)
    	{
    		s = p;
    		p = p->next;
    		s->next = sL;
    		sL = s;
    	}
    }
    

2.2 循环链表

  • 与单链表唯一的区别是,表尾的结点指向不是NULL。

    • 带头结点

      • 表为结点指向头结点
    • 不带头结点

      • 表尾结点指向首元结点
  • 基于这样一种变化,对于查询、遍历等循环结束条件,与链表是否为空的判断条件,由头结点next域或头指针是否为NULL,改成是否为头结点或首元结点

2.3 双向循环链表

  • 相对于单链表

    • 优势是,对于头插法的操作,不需要从表头开始进行遍历查找,可以直接找到前驱。
    • 劣势是,多维护一个前驱的关系,在某些情况可能比单链表耗性能。
  • 存储结构定义

    • 基于单链表结构的优化,反映链式存储关系;
    • 数据域定义与单链表无不同;
    • 指针域的定义,除了有指向后继结点元素的只针,还有指向前驱结点元素的只针;
    // 定义返回状态及返回码
    #define Status int
    #define OK 1
    #define ERROR 0
    // 定义存储的元素类型
    #define ElementType int
    
    /* 双向链循环表存储结点*/
    typedef struct DuL_Node
    {
    	// 指向前驱节点指针
    	DuL_Node* prior;
    	// 存储元素
    	ElementType data;
    	// 指向后继节点指针
    	DuL_Node* next;
    }DuNode, *DuL;
    

常用操作

2.3.1 初始化操作
  • 前驱与后继指针都指向头结点

    /* 01_双向循环链表——初始化_带头结点*/
    Status initList_DuL(DuL& duL)
    {
    	duL = (DuL)malloc(sizeof(DuL_Node));
    	if (!duL)
    	{
    		return ERROR;
    	}
    	duL->data = NULL;
    	// 循环链表,初始前置和后继时候指向自己
    	duL->prior = duL;
    	duL->next = duL;
    	return OK;
    }
    
2.3.2 指定索引位置前插入元素
  • 不需要通过前驱结点来判断索引位置结点,找到后就直接对索引位置进行操作。

    /* 02_双向循环链表——在指定索引位置前插入元素*/
    Status insertByIndex_DuL(DuL& duL, int index, ElementType data)
    {
    	// 定义:插入之前的元素结点
    	DuL beforeInsert = duL;
    	// 先找到待插入为值的前置结点
    	for (int i=0;i<index;i++)
    	{
    		// 如果索引大于链表元素个数,则直接放最后
    		if (beforeInsert->next != duL)
    		{
    			beforeInsert = beforeInsert->next;
    		}
    	}
    	// 插入后的后继节点
    	DuL afterInsert = beforeInsert->next;
    	// 建立插入结点
    	DuL curInsert = (DuL)malloc(sizeof(DuL_Node));
    	if (!curInsert)
    	{
    		return ERROR;
    	}
    	curInsert->data = data;
    
    	// 与前置结点关联
    	beforeInsert->next = curInsert;
    	curInsert->prior = beforeInsert;
    	// 与后继结点关联
    	curInsert->next = afterInsert;
    	afterInsert->prior = curInsert;
    
    	return OK;
    }
    
2.3.3 遍历

访问方法简单定义

/* 05_双向循环链表——访问*/
void visit(DuNode duNode)
{
	printf("duNode data -> %d\n", duNode.data);
}
1. 正向遍历
  • 通过next后继指针遍历

    /* 03_双向循环链表——正序遍历*/
    void traverseList_DuL(DuL duL)
    {
    	DuL p = duL->next;
    	while (p != duL)
    	{
    		visit(*p);
    		p = p->next;
    	}
    }
    
2. 反向遍历
  • 通过prior前驱指针遍历

    /* 04_双向循环链表——反向遍历*/
    void backTraverseList_DuL(DuL duL)
    {
    	DuL p = duL->prior;
    	while (p != duL)
    	{
    		visit(*p);
    		p = p->prior;
    	}
    }
    
2.3.4 批量插入元素到表尾
  • 访问正向或反向的表尾元素很简单,直接通过头节点的prior即可。

  • 相对于单链表而言,不需要从表头开始遍历到表尾。

    /* 06_双向循环链表——批量插入数据到表尾*/
    Status batchInsertToTail_DuL(DuL& duL, ElementType* datas, int length)
    {
    	// 待插入的位置
    	DuL tail = duL->prior;
    	// 待插入的结点迭代
    	DuL cur = NULL;
    	for (int i = 0; i < length; i++)
    	{
    		// 每次创建一个结点
    		cur = (DuL)malloc(sizeof(DuNode));
    		// 插入失败则返回错误
    		if (!cur)
    		{
    			return ERROR;
    		}
    		cur->data = *(datas + i);
    		// 连接前置结点
    		tail->next = cur;
    		cur->prior = tail;
    		// 连接后继结点
    		cur->next = duL;
    		duL->prior = cur;
    		// 下一个
    		tail = cur;
    	}
    	return OK;
    }
    
2.3.5 删除指定索引的元素
  • 不需要通过前驱结点访问到当前结点,因为双向的优势可以直接找前驱或者后继

    /* 07_双向循环链表——删除指定索引的元素*/
    ElementType deleteByIndex_DuL(DuL& duL, int index)
    {
    	ElementType data;
    	DuL cur = duL->next;
    	// 直接找到该结点,不用找前置
    	for (int i = 0; i < index; i++)
    	{
    		if (cur->next == duL)
    		{
    			return NULL;
    		}
    		cur = cur->next;
    	}
    	// 保存返回值
    	data = cur->data;
    	// 将前置节点和后继结点连接起来
    	cur->prior->next = cur->next;
    	cur->next->prior = cur->prior;
    	// 释放空间
    	free(cur);
    	return data;
    }
    

    暂时先补充到这里,如果有常见的操作会更新加到这里。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值