数据结构-第2节-顺序表和链表

目录

1.线性表

2.顺序表

2.1.概念及结构

2.2.接口实现(顺序表的增删查改)

2.3.顺序表相关练习

3.链表

3.1.链表的概念及结构

3.2.单链表接口实现(单链表的增删查改)

3.3.链表相关练习

3.4.链表的分类

3.5.双向 带头 循环 链表实现

4.顺序表和链表的区别


1.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。


2.顺序表

2.1.概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:静态顺序表 和 动态顺序表
顺序表优缺点:
优点:
1.连续物理空间,方便下标随机访问
缺陷:
1.插入数据,空间不足时要扩容,扩容有性能消耗(realloc函数进行扩容性能会有消耗)
2.头部或者中间位置插入删除数据,需要挪动数据,效率较低
3.可能存在一定空间占用,浪费空间(扩容一般是扩容到之前的二倍,但是扩容后的空间如果只需要一点点则存在大量空间浪费)
4.不能按需申请和释放空间(删除空间只是size--,并没有真正的释放空间)

1. 静态顺序表:使用定长数组存储元素

要求:存储的数据从0开始,依次连续存储

问题:内存开小了不够用,开大了存在浪费(不实用)

2.动态顺序表:使用动态开辟的数组存储

要求:存储的数据从0开始,依次连续存储

 注:指针a负责接收动态开辟出来的空间地址

2.2.接口实现(顺序表的增删查改)

test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

void TestSeqList1()
{
	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushBack(&s, 5);
	SeqListPushBack(&s, 0);
	SeqListPrint(&s);

	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPrint(&s);

	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPrint(&s);

	SeqListPushBack(&s, 10);
	SeqListPushBack(&s, 20);
	SeqListPrint(&s);

}

void TestSeqList2()
{

	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushFront(&s, 0);
	SeqListPushFront(&s, -1);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPushFront(&s, 0);
	SeqListPushFront(&s, -1);
	SeqListPrint(&s);
}

void TestSeqList3()
{
	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPrint(&s);

	SeqListInsert(&s, 10, 100);
	SeqListInsert(&s, 1, 20);
	SeqListInsert(&s, 5, 50);
	SeqListInsert(&s, 0, 50);

	SeqListPrint(&s);

	SeqListDestroy(&s);
}

void TestSeqList4()
{
	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushBack(&s, 5);
	SeqListPrint(&s);

	SeqListErase(&s, 4);
	SeqListPrint(&s);

	SeqListErase(&s, 0);
	SeqListPrint(&s);

	SeqListErase(&s, 1);
	SeqListPrint(&s);
}

void TestSeqList5()
{
	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushBack(&s, 5);
	SeqListPushFront(&s, -1);
	SeqListPushFront(&s, -2);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPopBack(&s);
	SeqListPopBack(&s);
	SeqListPrint(&s);

	SeqListDestroy(&s);
}

void TestSeqList6()
{
	SeqList s;
	SeqListInit(&s);
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushBack(&s, 5);
	SeqListPushFront(&s, -1);
	SeqListPushFront(&s, -2);
	SeqListPrint(&s);

	SeqListModify(&s, 3, 1000000);
	SeqListPrint(&s);

	int pos = SeqListFind(&s, 4);
	if (pos != -1)
	{
		SeqListModify(&s, pos, 40);
	}

	SeqListPrint(&s);

	SeqListDestroy(&s);
}

int main()
{

	TestSeqList6();

	return 0;
}

SeqList.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* a;
	int size;     // 存储数据个数
	int capacity; // 存储空间大小
}SL, SeqList;


//打印内存数据
void SeqListPrint(SeqList* psl);


//初始化内存数据
void SeqListInit(SeqList* psl);
//释放内存数据
void SeqListDestroy(SeqList* psl);


//检查内存容量
void SeqListCheckCapacity(SeqList* psl);


//尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
//尾删
void SeqListPopBack(SeqList* psl);


//头插
void SeqListPushFront(SeqList* psl, SLDataType x);
//头删
void SeqListPopFront(SeqList* psl);


// 任意插
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 任意删
void SeqListErase(SeqList* psl, size_t pos);


// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);


//顺序表修改
void SeqListModify(SeqList* psl, size_t pos, SLDataType x);

SeqList.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

void SeqListPrint(SeqList* psl)
{
	assert(psl);

	for (int i = 0; i < psl->size; ++i)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
}


void SeqListInit(SeqList* psl)
{
	assert(psl);

	psl->a = NULL;
	psl->size = 0;
	psl->capacity = 0;
}


void SeqListDestroy(SeqList* psl)
{
	assert(psl);
	free(psl->a);
	psl->a = NULL;
	psl->capacity = psl->size = 0;
}


void SeqListCheckCapacity(SeqList* psl)
{
	assert(psl);

	// 如果满了,我们要扩容
	if (psl->size == psl->capacity)
	{
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = realloc(psl->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			psl->a = tmp;
			psl->capacity = newCapacity;
		}
	}
}


void SeqListPushBack(SeqList* psl, SLDataType x)
{
	assert(psl);

	//方法1:尾插功能实现代码
	/*SeqListCheckCapacity(psl);

	psl->a[psl->size] = x;
	psl->size++;*/

	//方法2:用任意插功能实现尾插
	SeqListInsert(psl, psl->size, x);
}


void SeqListPopBack(SeqList* psl)
{
	assert(psl);

	//方法1:尾删功能实现代码
	//psl->a[psl->size - 1] = 0;
	/*if (psl->size > 0)
	{
		psl->size--;
	}*/

	//方法2:用任意删功能实现尾删
	SeqListErase(psl, psl->size - 1);
}


void SeqListPushFront(SeqList* psl, SLDataType x)
{
	assert(psl);

	//方法1:头插功能实现代码
	//SeqListCheckCapacity(psl);
	 挪动数据,腾出头部空间
	//int end = psl->size - 1;
	//while (end >= 0)
	//{
	//	psl->a[end + 1] = psl->a[end];
	//	--end;
	//}
	//psl->a[0] = x;
	//psl->size++;

	//方法2:用任意插功能实现头插
	SeqListInsert(psl, 0, x);
}

void SeqListPopFront(SeqList* psl)
{
	assert(psl);

	//方法1:头删功能实现代码
	// 挪动数据覆盖删除
	/*if (psl->size > 0)
	{
		int begin = 1;
		while (begin < psl->size)
		{
			psl->a[begin - 1] = psl->a[begin];
			++begin;
		}

		--psl->size;
	}*/

	//方法2:用任意删功能实现头删
	SeqListErase(psl, 0);
}


void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl);

	// pos数值温和检查
	if (pos > psl->size)
	{
		printf("pos 越界:%d\n", pos);
		return;
		//exit(-1);
	}
	// pos数值暴力检查
	//assert(pos <= psl->size);

	SeqListCheckCapacity(psl);

	//交换方法1(pos必须强制类型转换)
	//int end = psl->size - 1;
	//while (end >= (int)pos)
	//{
	//	psl->a[end + 1] = psl->a[end];
	//	--end;
	//}

	//交换方法2
	size_t end = psl->size;
	while (end > pos)
	{
		psl->a[end] = psl->a[end - 1];
		--end;
	}

	psl->a[pos] = x;
	psl->size++;
}


void SeqListErase(SeqList* psl, size_t pos)
{
	assert(psl);
	assert(pos < psl->size);

	size_t begin = pos + 1;
	while (begin < psl->size)
	{
		psl->a[begin - 1] = psl->a[begin];
		++begin;
	}

	psl->size--;
}


int SeqListFind(SeqList* psl, SLDataType x)
{
	assert(psl);

	for (int i = 0; i < psl->size; ++i)
	{
		if (psl->a[i] == x)
		{
			return i;
		}
	}

	return -1;
}


void SeqListModify(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl);
	assert(pos < psl->size);

	psl->a[pos] = x;

}

注:

1.传参数的时候不要传值调用(如下图所示),因为形参是实参的一份临时拷贝,对形参修改并不会影响实参,并且结构体传值调用会消耗大量的内存空间,因此要传值调用

2.系统对于越界的查询其实是抽查,有时候越界了但不一定能检查出来,就像查酒驾一样,不一定会被查到,但是我们一定不能酒驾

3.可以看出头插和头删时间复杂度为O(N),尾插和尾删时间复杂度为O(1),因此尽量使用尾插和尾删,不到万不得已不要使用头插和头删

4.静态的数组是在操作结束的时候就检查越界,动态开辟的内存只有在free的时候才会检查越界,如果不写free系统不会检查。系统在调试的时候如果调试到free进行报错,如果空间地址起始没有写错的话,那就是前面有越界的情况,这是一个经典的问题

5.尾插可以用任意位置插来实现(pos=size),头插可以用任意位置插来实现(pos=0)

   尾删可以用任意位置删来实现(pos=size-1),头删可以用任意位置删来实现(pos=0)

6.上面代码实现了顺序表的增删改查功能

7.可以自己尝试一下给各功能写一个菜单,如下图所示,注意写菜单最好是所有功能都实现了再写菜单因为一上来就写菜单不好调试

2.3.顺序表相关练习

练习1:

习题链接:27. 移除元素 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.每查找到一个val,挪动后面的数据向前进行覆盖

时间复杂度(最坏的情况,即全是val):O(N^{2})     空间复杂度:O(1)

2.把不是val的值拷贝到新数组中,然后将新数组中的值拷贝回原数组

时间复杂度:O(N)     空间复杂度:O(N)

3.使用两个指针src和dst都指向下标为0的元素,src++往后遍历,如果src解引用不是val就放到dst中然后dst++src++,如果src解引用是val,直接src++

时间复杂度:O(N)     空间复杂度:O(1)

代码:(思路三代码实现)

练习2:

  习题链接:26. 删除有序数组中的重复项 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.使用两个指针src和dst都指向下标为0的元素,src++往后遍历,如果src解引用不等于src+1的解引用,就将src放到dst中然后dst++src++;如果src解引用等于src+1解引用,直接src++

时间复杂度:O(N)     空间复杂度:O(1)

2.使用两个指针src和dst,src指向下标为1的元素,dst指向下标为0的元素,src往后遍历,如果src解引用不等于src-1的解引用,就将src-1解引用放到dst中然后dst++src++;如果src解引用等于src-1的解引用,直接src++(该方法在判断最后一个值的时候无法判断因为要用最后一个值的后面与其比较,这样会造成越界访问。因为最后一个值如果和倒数第二个值相等,倒数第二个值没有拿到dst中,最后一个值应该拿到dst中;最后一个值如果和倒数第二个值不相等,最后一个值应该放到dst中;所以最后一个值不管什么情况都应该放到dst中)(具体实现代码见下面代码1)

时间复杂度:O(N)     空间复杂度:O(1)

3.使用三个指针dst、cur、next,dst和cur指向下标为0的元素,next指向下标为1的元素,next用来跳过所有相同的元素,具体实现过程见下面代码2

时间复杂度:O(N)     空间复杂度:O(1)

代码1:(思路2代码实现)

 代码2(思路3代码实现)

int removeDuplicates(int* nums, int numsSize){
int cur=0;
int dst=0;
int next=1;
while(next<numsSize)
{
    if(nums[cur]==nums[next])
    {
        next++;
    }
    else
    {
    nums[dst]=nums[cur];
    dst++;
    cur=next;
    next++;

    }
}
    nums[dst]=nums[numsSize-1];
    dst++;
return dst;

}

练习3:

习题链接:88. 合并两个有序数组 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.memmove+sort(冒泡或qsort函数)

冒泡时间复杂度:O(N^{2})          qsort时间复杂度:O(N*log^{N})     

2.归并排序,使用两个指针i和j分别指向两个数组sums1和sums2下标为0的元素,创建一个数组a,两个指针解引用进行比较,将数值较小的值放到数组a中,然后数值较小值的指针++;如果两个指针解引用相等,则任意一个指针解引用的数值放入a中即可,然后该指针++;到最后其中一个数组走完了,那么就把另一个数组剩下的元素全部放到a中

时间复杂度:O(m+n)         空间复杂度O(m+n)

3.使用归并排序的思想从大往小进行归并,不用创建新的数组。指针i指向nums1的最后一个元素即第m个元素地址,指针j指向nums2的最后一个元素即第n个元素地址,指针dst指向nums1中m+n,

两个指针i和j解引用进行比较,将数值较大的值放到指针dst指向的地址中,然后数值较大值的指针dst--;如果两个指针解引用相等,则任意一个指针解引用的数值放入dst指向空间即可,然后该指针dst--;到最后如果nums2先走完,那么就结束了,如果nums1先走完,把nums2中剩下的数据按顺序放入nums1剩下的内存中即可

时间复杂度:O(m+n)         空间复杂度O(1)

代码(思路3代码实现)

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i=m-1;
int j=n-1;
int dst=m+n-1;
while(i>=0&&j>=0)
{
    if(nums1[i]>nums2[j])
    {
    nums1[dst]=nums1[i];
    dst--;
    i--;
    }
    else
    {
    nums1[dst]=nums2[j];
    dst--;
    j--;
    }
}

 while(j>=0)
 {
     nums1[dst]=nums2[j];
     dst--;
      j--;
  }

}


3.链表

3.1.链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 

构造链表(单链表): 

 ​​​​

逻辑结构图(单链表):

物理结构图(单链表):

链表打印功能实现代码(单链表):

注:

1.打印的时候,如下图所示,因为打印一个空的链表无非就是不打值出来,但是assert断言如果是空链表那么程序直接报错,不合理,一定不能为空,才用断言。

3.2.单链表接口实现(单链表的增删查改)

 单链表结构特点:

1.适合头插头删

2.尾部或中间某个位置插入删除效率较低

3.如果使用链表单独存储数据,使用后面学的双向链表更合适

单链表学习意义:

1.单链表会作为我们以后学习复杂数据结构的子结构(图的邻接表、哈希桶)

2.单链表会出很多经典的练习题

代码:

test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

void TestSList1()
{
	
	SListNode* slist = NULL;
	SListNode* n1 = malloc(sizeof(SListNode));
	SListNode* n2 = malloc(sizeof(SListNode));
	SListNode* n3 = malloc(sizeof(SListNode));
	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n1->next = n2;
	n2->next = n3;
	n3->next = NULL;
	slist = n1;

	SListPrint(slist);
}



void TestSList2()
{
	SListNode* slist = NULL; //空链表

	for (int i = 0; i < 4; ++i)
	{
		SListPushBack(&slist, i);
	}
	SListPrint(slist);

}

void TestSList3()
{
	SListNode* slist = NULL; //空链表
	SListPushBack(&slist, 1);
	SListPushBack(&slist, 2);
	SListPushBack(&slist, 3);
	SListPushBack(&slist, 4);

	SListPopBack(&slist);
	SListPopBack(&slist);
	SListPopBack(&slist);
	SListPopBack(&slist);
	SListPopBack(&slist);
	SListPrint(slist);
}

void TestSList4()
{
	SListNode* slist = NULL; //空链表
	SListPushBack(&slist, 1);
	SListPushBack(&slist, 2);
	SListPushBack(&slist, 3);
	SListPushBack(&slist, 4);

	SListPopFront(&slist);

	SListNode* pos = SListFind(slist, 3);
	if (pos)
	{
		printf("找到了:%p\n", pos);
		// 修改
		pos->data *= 10;
	}

	SListPrint(slist);
}

void TestSList5()
{
	SListNode* slist = NULL; //空链表
	SListPushBack(&slist, 1);
	SListPushBack(&slist, 2);
	SListPushBack(&slist, 3);
	SListPushBack(&slist, 4);
	SListPrint(slist);

	SListNode* pos = SListFind(slist, 3);
	if (pos)
	{
		SListInsert(&slist, pos, 30);
	}
	SListPrint(slist);

	pos = SListFind(slist, 1);
	if (pos)
	{
		SListInsert(&slist, pos, 30);
	}
	SListPrint(slist);
}

void TestSList6()
{
	SListNode* slist = NULL; //空链表
	SListPushBack(&slist, 1);
	SListPushBack(&slist, 2);
	SListPushBack(&slist, 3);
	SListPushBack(&slist, 4);
	SListPushBack(&slist, 5);
	SListPrint(slist);

	SListNode* pos = SListFind(slist, 3);
	if (pos)
	{
		SListErase(&slist, pos);
	}
	SListPrint(slist);

	pos = SListFind(slist, 1);
	if (pos)
	{
		SListErase(&slist, pos);
	}
	SListPrint(slist);
}

int main()
{
	TestSList6();

	return 0;
}

SList.h文件:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data; // val
	struct SListNode* next; // 存储下一个节点的地址
}SListNode, SLN;

//打印
void SListPrint(SListNode* phead);

//尾插
void SListPushBack(SListNode** pphead, SLTDataType x);

//头插
void SListPushFront(SListNode** pphead, SLTDataType x);

//尾删
void SListPopBack(SListNode** pphead);

//头删
void SListPopFront(SListNode** pphead);

//查找
SListNode* SListFind(SListNode* phead, SLTDataType x);

// 任意位置pos前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x);

// 删除任意pos处节点
void SListErase(SListNode** pphead, SListNode* pos);

// 任意位置pos后插入
void SListInsertAfter(SListNode* pos, SLTDataType x);

// 删除任意pos处后面节点
void SListEraseAfter(SListNode* pos);

//释放链表内存数据
void SListDestroy(SListNode** pphead);

SList.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"


void SListPrint(SListNode* phead)
{
	SListNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}


SListNode* BuySListNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
	}

	return newnode;
}


void SListPushBack(SListNode** pphead, SLTDataType x)
{
	assert(pphead);

	SListNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SListNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;

	}
}


void SListPushFront(SListNode** pphead, SLTDataType x)
{
	assert(pphead);

	SListNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}


void SListPopBack(SListNode** pphead)
{
	assert(pphead);

	// 1、空
	// 2、一个节点
	// 3、多个节点

	// 暴力检查为空的情况
	//assert(*pphead != NULL);
	if (*pphead == NULL)  // 温柔检查
	{
		return;
	}
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//方法1
		/*SListNode* prev = NULL;
		SListNode* tail = *pphead;
		while (tail->next != NULL)
		{
		prev = tail;
		tail = tail->next;
		}

		free(tail);
		tail = NULL;
		prev->next = NULL;*/


		//方法2
		SListNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}


void SListPopFront(SListNode** pphead)
{
	assert(pphead);

	// 1、空
	// 2、非空
	if (*pphead == NULL)
	{
		return;
	}
	else
	{
		SListNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;
	}
}


SListNode* SListFind(SListNode* phead, SLTDataType x)
{
	SListNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}


// 任意位置pos前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);

	// 1、pos是第一个节点
	// 2、pos不是第一个节点
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SListNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}


// 删除任意pos处节点
void SListErase(SListNode** pphead, SListNode* pos)
{
	assert(pphead);
	assert(pos);

	if (*pphead == pos)
	{
		SListPopFront(pphead);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}


void SListInsertAfter(SListNode* pos, SLTDataType x)
{
	assert(pos);

	//方法1
	//SListNode* next = pos->next;
	//SListNode* newnode = BuySListNode(x);
	//pos->next = newnode;
	//newnode->next = next;

	//方法2
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}


void SListEraseAfter(SListNode* pos)
{
	assert(pos);

	SListNode* next = pos->next;
	if (next)
	{
		pos->next = next->next;
		free(next);
		next = NULL;
	}
}

void SListDestroy(SListNode** pphead)
{
	assert(pphead);

	SListNode* cur = *pphead;
	while (cur)
	{
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}

注:   

1.链表不需要专门初始化,因为链表最开始的时候是一个空链表,只需要有一个指针指向NULL即可,不会用到结构体

2.头插和尾插传参的时候应该传指针变量slist的地址,因此如果是空链表,第一次是要改变slist的值存储第一个数据的地址(只有第一次要改变slist,后面不再改变,因此后面是可以不用传地址的,但为了统一,都传地址并且用二级指针接收),如果传的是指针变量slist,那么形参是实参的一份临时拷贝,无法改变slist的值

头删和尾删传参也需要传指针变量slist的地址,因为如果只剩一个进行删除,需要将指针变量slist置成NULL

3.传指针变量slist的地址用二级指针变量接收的话,需要对二级指针变量assert,因为指针变量slist可能为NULL但二级指针变量一定不为NULL

4.尾删的时候下面代码是错误的,因为tail只是一个指针变量,虽然tail存储的地址和删除后最后一个结构体中的next中地址相同,可以成功free,但最后将tail置成NULL是无法改变最后一个结构体中的next的。

解决办法有两个:

第一种办法:使用两个指针变量tail和prev,tail每次跳到下一个的时候先将tail的值给prev,这样到最后结束prev中next置空即可,见下图左所示

第二种方法:每次循环判断tail的next的next是否为空,如果是空则将tail中的next置空即可,见下图右所示

  

但是上面代码也是有问题的,因为如果是一个节点(*pphead->next为NULL)或者没有节点(*pphead为NULL),那么上面代码是不成立的,因此需要分空、一个节点、多个节点进行讨论,如果是空的话可以直接assert暴力检查也可以直接返回,具体如下图所示

 

5.头删的时候也需要考虑一个节点、没有节点、多个节点的问题,进行分类讨论,如下图左所示;但是这里面只有一个节点和多个节点可以合并一起处理,因为如果只有一个节点的话,next为NULL,将next赋值给*pphead,并且进行释放和多个节点处理方式相同,如下图右所示

  

6.这里面修改函数和查找函数是一个意思,因为如果找到了要修改的数,可以直接pos->data进行修改即可,如下图所示

7.任意位置pos前插入是在给的地址对应结点之前插入新的结点,因为没办法从后一个结点找到前一个结点,因此需要用指针变量prev从头开始遍历,找到前一个结点的地址,但是这种方法不适用于给第一个元素前插入结点,因为给第一个元素前插入需要改变*pphead指向新增结点(也就是头插),所以要分两种情况,如下图所示

8.删除任意pos处的位置,用prev遍历找到前一个结点的地址,将前一个结点的next置成pos结点的next,并将pos释放pos的next置成NULL,但是这种方法不适用于删除第一个结点,删除第一个结点直接用头删即可,因此要分删除第一个结点和删除其他结点两个情况讨论,代码如下图所示

3.3.链表相关练习

练习1:

题目链接:203. 移除链表元素 - 力扣(LeetCode) (leetcode-cn.com)

思路:

使用cur往后遍历整个后面节点,prev跟着cur指向cur上一个节点,具体见代码

需要考虑几种极端情况:

我们发现第二种极端情况使用常规情况的处理方法可以解决,因此不用单独考虑。第三种情况prev初始为NULL,无法取空地址的next并进行赋值,所以要在常规情况中对此进行改进

代码:

注:

1.像这种接口题做题时先写好自己的代码然后提交,将无法通过样例代入自己写的代码分析代码错误;如果无法分析出来错误,那么就去编译器下调试,像这种接口型题目不好调试,我们可以在主函数中自己建立链表手动进行链接,如下图所示 。样例7 7 7 7出错,手动连接代码如下图所示

struct ListNode
{
	int val;
	struct ListNode *next;
};

struct ListNode* removeElements(struct ListNode* head, int val){
	struct ListNode* prev = NULL;
	struct ListNode* cur = head;
	while (cur)
	{
		if (cur->val != val)
		{
			prev = cur;
			cur = cur->next;
		}
		else
		{
			struct ListNode* next = cur->next;
			free(cur);
			prev->next = next;
			cur = next;
		}
	}

	return head;
}

int main()
{
	struct ListNode* node1 = malloc(sizeof(struct ListNode));
	struct ListNode* node2 = malloc(sizeof(struct ListNode));
	struct ListNode* node3 = malloc(sizeof(struct ListNode));
	struct ListNode* node4 = malloc(sizeof(struct ListNode));
	node1->val = 7;
	node2->val = 7;
	node3->val = 7;
	node4->val = 7;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	node1 = removeElements(node1, 7);

	return 0;
}

2.可以看出头删还有一种方法是将头指针变量指向后面元素的地址即可,然后返回头指针变量,返回的头指针变量是新的头(该方法不适合应用于单链表接口实现,因为接口实现中函数一般不返回头指针,头指针也一般不会改变,像做这种练习题时删除一些元素并且头跟着改变了返回新的头指针时是可以使用的)

练习2:

 

题目链接:206. 反转链表 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.使用三个指针变量n1、n2、n3。n1初始为NULL用于存储前一个节点地址,n2初始值为第一个节点地址用于将n2->next更新成n1地址,n3初始值为第二个节点地址用于存储n2往后遍历的节点地址。最后n2如果等于NULL循环结束并且新的头就是n1存储的地址。

最后要注意n3最后等于NULL会访问空的next,要进行处理

2.头插法:定义指针变量cur初始值为原链表的头,该指针变量cur变量链表取原链表节点,头插到newHead所在的链表,定义指针变量newHead初始值为NULL,每一次进行头插之后newHead指向新头插的节点,定义指针变量next初始值为第二个节点的地址,帮助cur往后进行遍历。到最后cur为空则循环结束并且newHead是新的头

代码(思路1实现):

代码(思路2实现):

练习3:

题目链接:876. 链表的中间结点 - 力扣(LeetCode) (leetcode-cn.com) 

思路:

1.快慢指针:指针fast走的永远是slow的两倍,那么fast走到最后一个节点或者走到空(最后一个节点的next)此时slow就在中间(奇数是中间,偶数是中间后一个)

代码(思路1代码实现):

注:

1.上面思路1实现的代码中while循环截至条件中不能将并符号左右式子交换,因为

fast->next && fast 如果节点为偶数,那么fast最后跳完两个后指的为NULL,此时判断先判断fast->next,程序会崩溃。写成 fast && fast->next 就保证了后面的fast不为空

练习4:

 题目链接:链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

思路:

1.快慢指针:指针变量fast先走k步,然后slow和fast同时走,fast走到空时,slow就是倒数第k个。要考虑k大于链表长度的情况

代码:

练习5:

题目链接:21. 合并两个有序链表 - 力扣(LeetCode) (leetcode-cn.com) 

思路:

1.定义指针变量head作为新链表的头,list1和list2往后遍历并进行比较,将较小的值尾插给以head尾头的新链表,定义指针变量tail指向每次尾插最后一个节点的地址。最后当list1和list2中一个为空就将另一个后面的链表整个尾插到新链表的后面。

因为head和tail初始值都为NULL,在尾插的时候我们得考虑到开始时head和tail都是NULL的情况

要考虑到给的两个链表是否有链表一开始就为空,不考虑的话tail为空后面会取空的next程序崩溃

2.使用带头(节点)单链表,我们就不用考虑在尾插时开始时head和tail都是NULL的情况,因为此时有哨兵头节点,head和tail都不为NULL,其指向哨兵头节点地址。我们也不用考虑给的两个链表是否有链表一开始就为空的情况,因为有哨兵头节点tail不可能为空

注意此时返回的新头应为head->next

注意要对head进行释放,否则内存泄漏

代码(思路1实现):

代码(思路2实现):

注:

1.单链表根据头不同可以分为两种:无头(节点)单链表、带头(节点)单链表

如果不说某个链表是带头(节点)单链表那么就是无头(节点)单链表

其中带头(节点)单链表的第一个节点叫做哨兵位的头节点,这个节点不存储有效数据

在遍历链表时无头(节点)单链表从head往后遍历,遍历带头(节点)单链表从head->next往后遍历

练习6:

例子:       3  5  1  6  3  4

排序之后:3  1  3  5  6  4

题目链接: 链表分割_牛客题霸_牛客网 (nowcoder.com)

思路:

1.使用带头(节点)单链表,遍历原链表,把小于x的插入到一个链表1,把大于等于x的插入到一个链表2,然后链表1和链表2链接起来即可。定义指针变量lessHead和greaterHead分别指向链表1和链表2哨兵位的头节点地址,定义指针变量lesstail和greatertail分别指向链表1和链表2尾插最后一个节点的地址

我们思考极端场景:(1)所有值比x小 (2)所有值比x大 (3)比x大和小都有,最后一个值是比x小 (4)比x大和小都有,最后一个值是比x大

我们可以看出如果倒数第二个值比x大,最后一个值比x小的话,倒数第二个的next存的还是倒数第一个小的地址。导致最后链表1和链表2链接起来后链表中有一个循环(最后一个值如果不给greaterHead,那么经过链接之后链表最后一个节点的next就不是空,而是会导致一个循环)

如下:

3  5  1  6  3

 所以最后应该给最后的greatertail指针变量置成NULL

代码(思路1实现):

练习7:

 题目链接:链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

思路:

1.利用前面练习题写的找中间函数和逆置函数,先利用找中间值函数找到中间的节点rHead,再从中间节点rHead开始对链表进行逆置。经过逆置后比较从A开始和rHead开始的链表,直到rHead链表走完为止,如果都相同则返回true,如果在走完之前有一个不同就返回false

1 2 2 1     ->    中间节点:2     ->    逆置后:1 2

A开始的链表:1 2 2        rHead开始的链表:1 2    (在rHead结束之前data都相等,返回true)

1 2 3 2 1     ->    中间节点 :3     ->    逆置后:1 2 3

A开始的链表:1 2 3        rHead开始的链表:1 2 3    (在rHead结束之前data都相等,返回true)

代码(思路1代码实现):

注: 

1.如果要在vs编译器下进行调试,因为上面代码是c++代码,要进行调试可以使用之前的方法并且在c环境下把c++多余的代码删除进行调试,或者在c++环境下调用函数有一些不同,改成如下图所示进行调用即可

 练习8:

题目链接:160. 相交链表 - 力扣(LeetCode) (leetcode-cn.com) 

思路:

1.A链表的每个节点跟B链表依次比较,如果有相等,就是相交,第一个相等就是交点

时间复杂度:O(M*N)

2.A链表找到尾节点,B链表找到尾节点,尾节点的地址相同就是相交

时间复杂度:O(M+N)

求出两个链表的长度La和Lb,长的先走\left | Lb-La \right |,再一起走,第一个地址相等的就是交点

时间复杂度:O(M+N)

代码(思路2实现):

注:

1.编译器不能分析程序的执行逻辑而是分析语法逻辑(程序员写出代码,代码有其执行逻辑;编译器相当于翻译官,跑你的语法逻辑,翻译成机器能听懂的语言;最后CPU进行执行,会跑你的执行逻辑),如上面代码逻辑上不用加最后的return NULL,但是不加return NULL编译器无法通过,会报void没有返回值的错误(编译器认为如果上面两个if如果判断失败跳出来会没有返回值,但实际上肯定有一个if判断成功)

练习9:

 题目链接:141. 环形链表 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.快慢指针:定义两个指针变量slow和fast,slow一次走一步fast一次走两步。fast先进环,slow后进环,slow进环以后,fast开始追赶模式追赶slow,如果最后fast追赶上了slow,那么就有环

代码:

注:

1.循环链表:链表的尾节点接到链表的头

带环链表:链表的尾节点接到链表任意一个节点

拓展追问:

追问1:slow一次走1步,fast一次走2步,一定能追上吗?请证明

证明:slow进环,开始追击。假设slow进环以后,slow和fast之间的距离是N,每次追击,距离缩小1,当距离缩小为0时就追上了

 

追问2:slow一次走1步,fast一次走3步,能追上吗?fast一次走4步呢?n步?请证明

证明:slow一次走一步,fast一次走n步,不一定能追上,特殊场景下可能会永远追不上,如下:

slow一次走1步,fast一次走3步:

因此这种情况下:N是偶数能追上,N是奇数追不上

slow一次走1步,fast一次走4步:

因此这种情况下:N是3的倍数能追上,N不是3的倍数那么判断C-1或C-2是否是3的倍数,以此类推(C-1或C-2到后面经过一轮或几轮有可能能追上,变成3的倍数,也存在永远追不上的情况,这里面变化是很多的)

追问3:请求出链表环的入口点(使用slow一次走一步,fast一次走两步方法)

 L=N*C-X该式子等价于L=(N-1)*C+(C-X),从这个式子我们可以得出结论,一个指针从meet走,一个指针从head走,他们会在入口点相遇,此结论可以解决下面练习10

练习10:

题目链接:142. 环形链表 II - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.一个指针从meet走,一个指针从head走,他们会在入口点相遇,之前分析的方法解决

2.将环中的meet相遇节点断开,相遇点meet变成尾,相遇点的下一个点变成新的头headB,就是先将meet->next赋值给headB,然后将meet->next置成NULL,此时就将求环的入口点问题转换成了求相交链表的交点问题(前面练习题讲过),如下图所示

代码:

练习11:

 题目链接:138. 复制带随机指针的链表 - 力扣(LeetCode) (leetcode-cn.com)

描述:

1.先赋值含data和next的链表,在处理random。使用指针cur遍历原链表,每指向一个原节点malloc一个新节点data进行复制并将新节点尾插到新链表中,这样新链表整体架构完成。核心问题是处理random,查找原链表中每个节点的random是第几个节点,新链表中对应的节点就指向对应的第几个节点

时间复杂度:O(N^{2})

2.顺藤摸瓜法

第一步:拷贝节点链接在原节点后面

第二步:拷贝节点的random在原节点random的后面(原节点指向自己同样成立)

第三步:把拷贝节点解下来,链接到一起组成复制链表

时间复杂度:O(N)

代码(思路2代码实现):

上面这个图要加定义copyHead和copyTail定义

3.4.链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1.单向/双向

2.带哨兵位头/不带哨兵位头

3.循环/非循环

注:循环是最后一个指向第一个,带环是最后一个指向任意一个

三种性质结合,总共八种链表结构:

1.单向 带哨兵位头 循环

2.单向 带哨兵位头 非循环  

3.单向 不带哨兵位头 循环

4.单向 不带哨兵位头 非循环  (常用)(结构最简单)(OJ题中常出现/复杂数据结构的子

                                                                                          结构:哈希桶、图的邻接表)

5.双向 带哨兵位头 循环  (常用)(结构最复杂)(实际中最实用的链表结构)(STL中

                                                                                   list的结构)

6.双向 带哨兵位头 非循环

7.双向 不带哨兵位头 循环

8.双向 不带哨兵位头 非循环

3.5.双向 带头 循环 链表实现

test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void TestList1()
{
	//LTNode* pList = NULL;
	//ListInit(&pList);
	LTNode* pList = ListInit();
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);

	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	//ListPopBack(pList);
	ListPrint(pList);
}

void TestList2()
{
	//LTNode* pList = NULL;
	//ListInit(&pList);
	LTNode* pList = ListInit();
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListInsert(pos, 30);
	}
	ListPrint(pList);
}

void TestList3()
{
	LTNode* pList = ListInit();
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		//ListErase(NULL);
		ListErase(pos);
		pos = NULL;
	}
	ListPrint(pList);

	//ListErase(pList);
	//ListPrint(pList);
}

void TestList4()
{
	LTNode* pList = ListInit();
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);

	ListPushFront(pList, 0);
	ListPushFront(pList, -1);
	ListPushFront(pList, -2);
	ListPrint(pList);

	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);

	ListPopFront(pList);
	ListPopFront(pList);
	ListPopFront(pList);
	ListPrint(pList);

	ListDestory(pList);
	pList = NULL;
}

int main()
{
	TestList3();

	return 0;
}

List.h文件:

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;


//获取一个节点
LTNode* BuyLTNode(LTDataType x);

//链表打印
void ListPrint(LTNode* phead);

//初始化链表(两种初始化方法)
//void ListInit(LTNode** pphead);
LTNode* ListInit();


//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//尾删
void ListPopBack(LTNode* phead);

//头插
void ListPushFront(LTNode* phead, LTDataType x);
//头删
void ListPopFront(LTNode* phead);

//查找
LTNode* ListFind(LTNode* phead, LTDataType x);

//任意点pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);

//删除任意点pos位置的节点
void ListErase(LTNode* pos);

//对链表进行销毁
void ListDestory(LTNode* phead);

List.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

void ListPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n\n");
}

//void ListInit(LTNode** pphead)
//{
//	assert(pphead);
//	*pphead = BuyLTNode(0);
//	(*pphead)->next = *pphead;
//	(*pphead)->prev = *pphead;
//}

LTNode* ListInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	//方法1
	//LTNode* tail = phead->prev;
	//LTNode* newnode = BuyLTNode(x);

	//tail->next = newnode;
	//newnode->prev = tail;

	//newnode->next = phead;
	//phead->prev = newnode;

	//方法2
	ListInsert(phead, x);
}

void ListPopBack(LTNode* phead)
{
	assert(phead);
	// 链表为空
	assert(phead->next != phead);

	//方法1
	/*	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tail = NULL;

	tailPrev->next = phead;
	phead->prev = tailPrev;*/

	//方法2
	ListErase(phead->prev);
}

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	ListErase(phead->next);
}

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//方法1
	/*LTNode* newnode = BuyLTNode(x);
	pos->prev->next = newnode;
	newnode->prev = pos->prev;

	pos->prev = newnode;
	newnode->next = pos;*/

	//方法2
	LTNode* newnode = BuyLTNode(x);
	LTNode* posPrev = pos->prev;

	newnode->next = pos;
	pos->prev = newnode;
	posPrev->next = newnode;
	newnode->prev = posPrev;
}

void ListErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	free(pos);
	//pos = NULL;

	prev->next = next;
	next->prev = prev;
}

void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
	//phead = NULL;
}

注:

1.所有传的phead是不能为空的,因为即使没有新节点,也一定是有头节点的,所以用assert断言

2.如果一个节点也没有,头节点也要形成循环,自己指向自己 

3.尾插时我们改的是哨兵位这个节点的结构体内的结构体元素,因此传参只需要传结构体的地址即可,不用使用二级指针(这里不需要使用二级指针是哨兵位的功劳,如果单链表也加一个哨兵位的头节点,那么也可以用一级指针就解决问题)

4.双向带头循环链表头插和尾插可以用任意pos前插实现,见如下代码

头插:

尾插:

5.任意删ListErase函数实现,将pos置成NULL其实是无用的,因为函数实现里面的pos是形参,形参的改变不会影响实参(如果一定要实现,可以传pos的二级指针)(后面的ListDestory函数实现中pos置成NULL与这里完全相同,是无用的)

因此在外面使用该函数的时候,用ListErase函数进行删除(ListDestory函数进行销毁)的同时,应该对对应的指针(ListDestory函数进行销毁时这里对应的指针是pList)进行置空(与free函数使用相似)

6.双向带头循环链表头删和尾删可以用任意删实现,见如下代码

头删:

尾删:


4.顺序表和链表的区别

两个结构是相辅相成的结构

顺序表

优点:

1.物理空间是连续的,方便用下标随机访问

2.CPU高速缓存命中率会更高(了解)

缺点:

1.由于需要物理空间连续,空间不够需要扩容,扩容本身有一定消耗。其次扩容机制还存在一定的空间浪费

2.头部或者中部插入删除,挪动数据,效率低  O(N)

链表:

优点:

1.任意位置插入删除数据效率高  O(1)

2.按需申请和释放空间

缺点:

1.不支持下标的随机访问。有些算法不适合在他上面进行。如:二分查找、排序等

注:

1.下图是计算机的存储体系,平时说的内存就是下图中的主存,平时说的硬盘就是下图中的本地二级存储(本地磁盘)

三级缓存+寄存器(eax、ebx、ebp、esp等):寄存器速度最快、三级缓存次之,他们的速度都要远快于内存;价格贵;带电存储

主存(内存):速度较快;价格较贵;带电存储

本地二级存储(本地磁盘)(硬盘):速度慢;价格便宜;不带电存储

2.编译链接后,生成可执行程序,CPU执行这个程序,CPU要去访问内存,但是CPU不会直接访问内存,因为他嫌弃内存速度太慢了,因此会把数据加载到三级缓存器或者寄存器(4或8byte小数据就放到寄存器,大数据就放到缓存)

3.CPU会看数据是否在缓存或寄存器,在的话就叫命中,直接访问;不在的话就叫不命中,会先把数据从内存加载到缓存或寄存器,再访问。第一次访问不命中,随后会把要访问的数据以及旁边的数据一起放到缓存或寄存器中(具体旁边多少数据放到缓存或寄存器中取决于硬件),随后如果访问旁边的数据就会命中,可以直接访问读取。顺序表物理空间连续,第一次不命中后会把后面的数据一起拿到缓存(因为周围数据较大所以放在缓存中,寄存器放的是4或8byte小数据)中,因此CPU高速缓存命中率高;因为链表的物理空间不连续并且是随机的,所以第一次命中后后面要读取的数据大概率不会命中,命中率低,并且第一次命中后把其周围的数据也放到缓存中,周围的数据大概率不会使用,会造成缓存污染

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
实验一: 1、(必做题)采用函数统计学生成绩:输入学生的成绩,计算并输出这些学生的最低分、最高分、平均分。 2、(必做题)采用递归和非递归方法计算k阶裴波那契序列的第n项的值,序列定义如下: f0=0, f1=0, …, fk-2=0, fk-1=1, fn= fn-1+fn-2+…+fn-k (n>=k) 要:输入k(1<=k<=5)和n(0<=n<=30),输出fn。 3、(选做题)采用递归和非递归方法解汉诺塔问题,问题描述如下: 有三根柱子A、B、C,在柱子A上从下向上有n个从大到小的圆盘,在柱子B和C上没有圆盘,现需将柱子A上的所有圆盘移到柱子C上,可以借助柱子B,要每次移动一个圆盘,每根柱子上的圆盘只能大的在下,小的在上。 要:输入n,输出移动步骤。 实验二: 1、(必做题)每个学生的成绩信息包括:学号、语文、数学、英语、总分、加权平均分;采用动态方法创建数组用于存储若干学生的成绩信息;输入学生的学号、语文、数学、英语成绩;计算学生的总分和加权平均分(语文占30%,数学占50%,英语占20%);输出学生的成绩信息。 2、(必做题)可以在数组末尾追加新学生的成绩信息;可以根据学号,删除该学生的成绩信息。 3、(选做题)可以根据学号或总分,升序排序学生的成绩信息。 实验三: 1、(必做题)每个学生的成绩信息包括:学号、语文、数学、英语、总分、加权平均分;采用链表存储若干学生的成绩信息;输入学生的学号、语文、数学、英语成绩;计算学生的总分和加权平均分(语文占30%,数学占50%,英语占20%);输出学生的成绩信息。 2、(必做题)可以在链表末尾追加新学生的成绩信息;可以根据学号,删除该学生的成绩信息。 3、(选做题)可以根据学号或总分,升序排序学生的成绩信息。 实验四: 1、(必做题)假设有序表中数据元素类型是整型,请采用顺序表或(带头结点)单链表实现: (1)OrderInsert(&L, e, int (*compare)(a, b)) //根据有序判定函数compare,在有序表L的适当位置插入元素e; (2)OrderInput(&L, int (*compare)(a, b)) //根据有序判定函数compare,并利用有序插入函数OrderInsert,构造有序表L; (3) OrderMerge(&La, &Lb, &Lc, int (*compare)()) //根据有序判定函数compare,将两个有序表La和Lb归并为一个有序表Lc。 2、(选做题)请实现: (1)升幂多项式的构造,升幂多项式是指多项式的各项按指数升序有序,约定系数不能等于0,指数不能小于0; (2)两个升幂多项式的相加。 实验五: 1、(必做题)假设栈中数据元素类型是字符型,请采用顺序栈实现栈的以下基本操作: (1)Status InitStack (&S) //构造空栈S; (2)Status Push(&S, e) //元素e入栈S; (3)Status Pop(&S, &e) //栈S出栈,元素为e。 2、(必做题)假设队列中数据元素类型是字符型,请采用链队列实现队列的以下基本操作: (1)Status InitQueue(&Q) //构造空队列Q; (2)Status EnQueue(&Q, e) //元素e入队列Q; (3)Status DeQueue (&Q, &e) //队列Q出队列,元素为e。 3、(必做题)请实现:对于一个可能包括括号{}、[]、()的表达式,判定其中括号是否匹配。 实验六: 1、(必做题)假设二叉树中数据元素类型是字符型,请采用二叉链表实现二叉树的以下基本操作: (1)根据二叉树的先序序列和中序序列构造二叉树; (2)根据先序遍历二叉树; (3)根据中序遍历二叉树; (4)根据后序遍历二叉树。 测试数据包括如下错误数据: 先序:1234;中序:12345 先序:1234;中序:1245 先序:1234;中序:4231 2、(必做题)对于一棵二叉树,请实现: (1)计算二叉树的叶子数目; (2)计算二叉树的深度。 3、(选做题)给定n个权值,请构造它们的最优二叉树(赫夫曼树)。 期末大实验: 一、任意长十进制整数加法 二、车厢调度 三、赫夫曼编/译码 四、无线广播部署

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值