Data structure sequence list, linked list, doubly linked list

Sequential lists, linked lists, and doubly linked lists are all linear lists. Linear, as the name suggests, connects data in a certain order like a rope. Linear lists are commonly used structures for our daily storage of data, and have different applications in different scenarios. . In fact, linear tables also include stacks and queues. However, for reasons of space, this article will only describe in detail the three types of linear tables, namely sequence table, single-way linked list, and doubly-linked list. They mainly include storage forms, implementation steps, and the relationship between them. the difference.

Table of contents

sequence table

Storage format of sequence table

Dynamic Realization of Sequence Table

Data insertion into sequential tables

Deletion of sequence table data

Checking and modifying the sequence table

singly linked list

Storage form of singly linked list

 Insertion of singly linked list data

Deletion of singly linked list data

Doubly linked list

Storage form of doubly linked list

 

Insertion and deletion of data in the lead doubly linked list

The difference between sequence list and linked list

Advantages and disadvantages of sequence list and linked list

What is cpu cache hit ratio


sequence table

Storage format of sequence table

Sequence table is a data form stored in order in memory, which requires continuity of memory addresses. Therefore, sequence table must be implemented by array, but the implementation method is divided into static implementation and dynamic implementation. Static implementation means that the size of the array is given, and the addition, deletion, and modification of data can only be performed in such a large space. Most applications maintain data when the size of the space is clearly given. The dynamic implementation can expand the capacity of the data according to the needs and realize the maintenance of the data. The static implementation is relatively easy, just set up an array of the data type of the specified size. We will focus on the dynamic implementation.

Dynamic Realization of Sequence Table

To realize the dynamic expansion of data capacity, you need to understand the dynamic memory development functions, which are malloc, calloc, and realloc in C language. You can inquire about the specific functions by yourself. Here we need to use realloc. To realize the management of the opened space and the function of automatically opening a larger space when the space is full, we need at least three variables. The first is a pointer variable of the data type, set it as DataType * p, which is used to maintain the space that has been opened up, the second variable is used to indicate how many data are currently stored, set it as int size, and the third variable is used To indicate how much data can be stored in total, set it as int capacity, when size == capacity, it means that the capacity is full and needs to be expanded, then use the realloc function to open up a larger space and then give the pointer to DataType * p To maintain, in order to facilitate the use of passing values, we encapsulate all these three variables into a structure.

Code

typedef  int  DateType;


typedef struct SeqList
{
	SLDateType * a; //用来维护已开辟的空间
	int size; //表中存放了多少个数据
	int capacity; //表中实际能存放的数据
}SL;


void SeqListInit(SL* ps)  //将封装后的结构体的值进行初始化
{
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = 0;
} 



void SeqListAddCapacity(SL* ps)  //实现顺序表的容量的动态开辟
{
	if (ps->capacity == ps->size)
	{
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDateType* tmp = (SLDateType*)realloc(ps->a, newcapacity * sizeof(SLDateType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
		tmp = NULL;
	}
}

Data insertion into sequential tables

There are three forms of inserting sequence table data. The first is head inserting, that is, inserting data at the first element position of the array, which requires all previously stored data to be moved backward one bit to give the newly inserted data Evacuate a position, the second is tail insertion, which inserts data at the end of the sequence table, and the third is insertion at a specified position, which requires the position to be inserted and the elements behind it to be moved backward one bit , to free up a spot. It can be seen that inserting data in the head will affect the whole body, so the efficiency of inserting data is very low. If you want to insert a data, you have to traverse the entire array, and the insertion at the specified position is not much better. According to the most The bad case is the same as the head insertion, the time complexity is O(n), only the tail insertion has the highest efficiency, and the time complexity is O(1)

Code implementation of three insertion methods

//前插
void SeqLIstPushFront(SL* ps, SLDateType x)
{
	SeqListAddCapacity(ps);
	for (int i = ps->size; i >0; i--)
	{
		ps->a[i] = ps->a[i-1];
	}
	ps->a[0] = x;
	ps->size++;
}


//任意位置插入
void SeqListInsert(SL* ps, SLDateType x, SLDateType y)
{
	SeqListAddCapacity(ps);
	for (int i = ps->size; i >= x; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[x - 1] = y;
	ps->size++;
}


//尾插
void SeqListPushBack(SL* ps, SLDateType x)
{
	SeqListAddCapacity(ps);
	ps->a[ps->size] = x;
	ps->size++;
}

Deletion of sequence table data

The deletion of the sequence table can be achieved by directly overwriting the element to be deleted. It is also divided into three deletion methods, front deletion, specified position deletion, and tail deletion. Front deletion is to move forward the elements behind the first element, overwrite the first element, and then size--, the deletion is completed. The deletion of the specified position is similar to the front deletion, and the elements to be deleted are all behind the element The element is overwritten, and the tail deletion is relatively simple. You only need to size--, which means that the element has been deleted

Code implementation of three deletion methods

//前删
void SeqListPopFront(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}

	if (ps->size > 0)
	{
		ps->size--;
	}
}


//任意位置删除
void SeqListPop(SL* ps, SLDateType x)
{
	for (int i = x; i < ps->size; i++)
	{
		ps->a[i-1] = ps->a[i];
	}
	ps->size--;
}


//尾删
void SeqListPopBack(SL* ps)
{
	if (ps->size > 0)     //这里防止空表要删除会使size变为负数
	{
		ps->size--;
	}
}

Checking and modifying the sequence table

The viewing of the sequence table is relatively simple, through the size control loop, compare whether the element to be viewed exists

Code

void SeqListCheck(SL* ps, SLDateType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			putchar('\n');
			printf("got it, the subscript is %d", i);
			return 0;
		}
	}
	printf("not found\n");
}

//通过这个查看和修改都能实现

singly linked list

Storage form of singly linked list

The one-way linked list can solve some problems in the sequence table. Let's analyze some problems in the sequence table just now.

1. When inserting and deleting data at the head\any position, the time complexity is O(n)

2. To increase capacity, you need to apply for new space, copy data, and release old space, which will consume a lot

3. The space for each expansion will cause a lot of waste

There are many types of one-way linked lists, circular, two pointers pointing to the head and the tail, etc. Here we only describe the most classic and basic one-way linked lists, master the basics, and understand the others

The storage form of the singly linked list usually includes the data type DataType to be stored, and the pointer variable Linck*next pointing to the next node

struct ListNode
{
	DataStyle val;
	struct ListNode* next;
}Linck;

 Insertion of singly linked list data

When using a one-way linked list, we need a pointer variable that always points to the first node of the linked list. This pointer is used to maintain the linked list. By dereferencing this pointer, the first node of the linked list can be found, and then other nodes can be found , There are also three forms of data insertion, head insertion, specified position insertion, and tail insertion.

Tail insertion is the easiest way. You only need to point the pointer of the last node of the linked list to the next node to the newly created node. It is simple and simple, but it is not easy to find the location of the last node. The storage form of the linked list is on the memory address. It is discontinuous, and it is impossible to achieve arbitrary access like an array. It is not necessary to traverse the linked list from the head node, so that the time complexity will come up.

Head insertion is more troublesome, mainly because the head inserts a new element, it is necessary to move the pointer phead pointing to the head node, and to change the value of the pointer variable phead, it must be realized by passing a secondary pointer, such as the variable int a; think If you want to change the value of a in another function, you need to pass the pointer of a to it. Similarly, if you want to change the address pointed to by the pointer variable, you have to use a secondary pointer to achieve the purpose of modification. Many beginners may I am confused here, and the secondary pointer may be a bit confusing. In future articles, I will list three methods that can avoid the use of secondary pointers

There are many situations that need to be considered when inserting at a specified position, because head insertion and tail insertion can also be designated positions. To take into account both situations at the same time, if you insert before a certain position in the middle, you need to remember the next pointer of the previous node at this position The value of the value is actually a very simple process, but it is difficult to understand when it is described. Simply put, in order to maintain the coherence of the linked list, it is not possible to directly point the next pointer of the previous node to the new node. The next pointer of the node connects the linked list behind

 Code

//创建一个新节点的函数实现,后面创建直接调用
struct ListNode* Buynewnode(DataStyle Data)
{
	struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
	if (NULL == newnode)
	{
		printf("malloc fail\n");
		return NULL;
	}
	newnode->val = Data;
	newnode->next = NULL;
	return newnode;
}


//头插的实现,注意,这里传二级指针是为了改变phead的值
void Linckpushfront(struct ListNode** pphead, DataStyle Data)
{
	     assert(pphead);
		 struct ListNode* newnode = Buynewnode(Data);
		  newnode->next = *pphead;
		*pphead = newnode;
}


//尾插的实现
void Linckpushback(struct ListNode** pphead, DataStyle Data)
{
	assert(pphead);
	struct ListNode* tmp = *pphead;
	if (*pphead == NULL)
	{
		tmp = Buynewnode(Data);
		*pphead = tmp;
	}
	else
	{
		while (tmp->next != NULL)
		{
			tmp = tmp->next;
		}
		tmp->next = Buynewnode(Data);
	}
}


//指定位置插入的实现
//pos是指定节点的指针
void LinckpushInsert(Linck** pphead, Linck * pos, DataStyle Data)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	Linck* tmp = *pphead;
	while (tmp->next != pos)
	{
		tmp = tmp->next;
	}
	Linck* newnode = Buynewnode(Data);
	if ((*pphead)->next == NULL)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		tmp->next = newnode;
		newnode->next = pos;
	}
}

Deletion of singly linked list data

The data deletion of the one-way linked list is also divided into head deletion, tail deletion, and specified location deletion. Everyone understands the truth, but it’s too repetitive, so let’s directly upload the code

//头删
void Linckpopfront(Linck** pphead)
{
	if (*pphead == NULL)
	{
		assert(*pphead != NULL);
	}
	Linck* tmp = *pphead;
	*pphead = (*pphead)->next;
	free(tmp);
}


//尾删
void Linckpopback(Linck** pphead)
{
	assert(pphead);
	assert(*pphead != NULL);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		 return;
	}
	Linck* tmp = *pphead;
	while (tmp->next->next != NULL)
	{
		tmp = tmp->next;
	}
	free(tmp->next);
	tmp->next = NULL;
}



//指定位置删
void LinckpopErase(Linck** pphead, Linck* pos)
{
	assert(pphead);
	assert(*pphead);
	Linck* tmp = *pphead;
	if (*pphead == pos)
	{
		Linckpopfront(pphead);
	}
	else
	{
		while (tmp->next != pos)
		{
			tmp = tmp->next;
			assert(pos);//检测pos是否传错了
		}
		tmp->next = pos->next;
		free(pos);
	}
}

Doubly linked list

Storage form of doubly linked list

When we introduced the one-way linked list, we solved the problem of waste of space occupied by the sequence table and the low efficiency of inserting data at the head/designated position, but this brought a new problem. The one-way linked list inserts data at the end When traversing the linked list, the time complexity is O(n), and the appearance of the leading bidirectional circular linked list better solves the problem of the sequential list without causing other problems. The lead bidirectional circular linked list is the most complex in the linked list structure, but it is easy to understand and use. The lead refers to the sentinel guard node head in the figure below. This node does not store data and is only used as a guide head.

 Why do you say that the leading bidirectional circular linked list can solve the problem very well? Take the tail insertion traversal of the one-way linked list as an example, the leading bidirectional circular linked list does not have this problem. The head node head does not store any data, but points to the position of the tail node. That is to say, if you want to insert the end, you can directly find the end node through the head to complete the insertion. There is no need to traverse, and the efficiency will be improved. However, because each node stores the pointers of the two nodes before and after, it consumes more space, that is, take space for time

typedef int DLinckDataType;

typedef struct DLincked
{
	struct DLincked*  previous;  //指向前一个节点
	struct DLincked*  next;      //指向后一个节点
	DLinckDataType Data;         //存放数据
} DLinck;

 

The above code is the definition of the leading two-way circular linked list node

When the leading two-way circular linked list in the above figure is empty, the front and rear pointers point to itself respectively.

Insertion and deletion of data in the lead doubly linked list

The insertion and deletion of the lead bidirectional circular linked list is essentially the same as the idea of ​​the one-way linked list. I won’t go into details here, just upload the code directly.

//创建新节点
DLinck* Buynewnode(DLinckDataType Data)
{
	DLinck* newnode = (DLinck*)malloc(sizeof(DLinck));
	if (newnode == NULL)
	{
		assert(newnode);
		return -1;
	}
	newnode->previous = NULL;
	newnode->next = NULL;
	newnode->Data = Data;
	return newnode;
}


//尾插
void DLinckpushback(DLinck* phead, DLinckDataType Data)
{
	DLinck* newnode = Buynewnode(Data);
	newnode->previous = phead->previous;
	phead->previous->next = newnode;
	phead->previous = newnode;
	newnode->next = phead;
}





//首插
void DLinckpushfront(DLinck* phead, DLinckDataType Data)
{
	DLinck* newnode = Buynewnode(Data);
	newnode->next = phead->next;
	newnode->previous = phead;
	phead->next->previous = newnode;
	phead->next = newnode;
}


//指定位置插
void DLinckpush(DLinck* phead, DLinck* pos, DLinckDataType Data)
{
	assert(pos);
	DLinck* newnode = Buynewnode(Data);
	newnode->next = pos;
	newnode->previous = pos->previous;
	pos->previous->next = newnode;
	pos->previous = newnode;
}


//指定位置删
void DLinckpop(DLinck* phead, DLinck* pos)
{
	assert(phead->next != phead);
	assert(pos);
	DLinck* posprevious = pos->previous;
	DLinck* posnext = pos->next;
	posprevious->next = pos->next;
	posnext->previous = pos->previous;
	free(pos);
}



//头删
void DLinckpopfront(DLinck* phead)
{
	assert(phead->next != phead);
	DLinck* tmp = phead->next;
	phead->next = tmp->next;
	tmp->next->previous = phead;
	free(tmp);
}



//尾删
void DLinckpopback(DLinck* phead)
{
	assert(phead->next != phead );
	DLinck* tmp = phead->previous;
	phead->previous = tmp->previous;
	tmp->previous->next = phead;
	free(tmp);
}

The difference between sequence list and linked list

Advantages and disadvantages of sequence list and linked list

Advantages of sequential tables:

1. High efficiency of tail insertion and tail deletion

2. Random access by array

3. The cpu cache hit rate is higher (compared to the linked list)

shortcoming:

Inefficient insertion of head and middle

The waste of space is serious

Advantages of linked list:

1. Inserting data at a specified location is highly efficient

2. Apply for memory on demand

3. Can solve some problems in the sequence table

shortcoming:

does not support random access

What is cpu cache hit ratio

  The following content should refer to the above picture

CPU processing speed and memory are not in the same order of magnitude. When CPU executes instructions, it will not directly access the memory, but first check whether the data is in the L1, L2, or L3 cache. If it is (that is, a hit), then directly access it . If not (that is, a miss), then load the data in memory into the cache, and then access

In order to improve access efficiency, when the memory loads information into the cache, it does not load one by one, but loads more data around it. The amount of loading depends on the hardware. The data storage of the array is continuous. If the loaded data If it is an array, then all the elements around the array will be loaded into it, so that when accessing other data in the array, it can be found directly in the cache, and the hit rate is very high.

As for the linked list, the storage address of each node in the memory is random and basically not continuous. This leads to the fact that when a node in the linked list is accessed, the loaded peripheral data will not be loaded from the memory to the cache. Other nodes containing the linked list, when you want to access other elements of the linked list, you have to reimport them from the memory to the cache, and the hit rate is obviously not as high as that of the sequence table

Guess you like

Origin blog.csdn.net/m0_61350245/article/details/126289195