重新拾起数据结构,废话不多讲。
就我目前所认识的,首先弄懂一系列的数据结构,指针,引用,形参,实参是重点,不然传参的时候很容易函数里是可以的,出了函数发现什么都找不到了。
准备工作
好的,首先,
指针:就是地址,用*表示,指向一块内存,指向的是变量的首地址。
在64bit的机器上,对所有类型的地址取sizeof发现都是8,也就是64bit。
我们可以这样理解,无论是什么变量,指针都是指向变量的第一块内存(一个字节),但是指针的类型决定了这个指针指向的是从这个首地址开始往后多少个字节的内存。
&取地址,与指针类似,取到的就是变量的首地址。
引用:区别于拷贝构造函数,如果一个变量是另一个变量的引用,他们代表了物理内存中的同一块地址,其中一个改变,第二个也会随之改变。
形参:在函数头中的形参,是调用函数的变量的复制品,函数执行的时候,会自动调用一个拷贝构造函数,复制调用函数的参数来在函数中参与运算,但是出了函数体之后形参会被释构,即如果传递形参,原来调用函数的变量在函数结束的时候不会改变;
实参,传递引用
&:引用,区别于形参,函数头中以变量的引用参与函数运算,这样函数中变量所进行的操作同样在调用函数的变量的身上也有所体现;而且这样相对节省了空间和时间,如果不想让调用函数的变量改变,可以使用const &(常量引用)
好的那我现在开始一个一个写起:
链表
typedef struct link {
int value;
struct link* next;
}Link;
链表要实现的功能:
尾插入,头插入,双向链表,循环链表,。。。。
首先是尾插入:
经过回想,然后理清思路:
避免不必要的麻烦我这样实现:
传参传引用
判断是否头结点为空,如果为空创建一个头结点
先创建新节点
然后拷贝一个头结点head_temp(注意不是引用)
创建一个head_temp的引用tail, 将tail遍历到尾部
tail的next指向新节点
好的解释一下,中间一步有点微妙。
我们传参传的是引用,为什么是引用,因为考虑到,如果头结点是空的,我们在函数中可以创建头结点,如果是形参,则做不到这一点,毕竟一个函数的实现要考虑两种不同的情况。
建议无论各种数据结构的各种插入都是先创建一个临时新结点,然后再指过去,避免思路混乱。
tail一定要是一个引用,因为我们最后要将tail的next指向新节点,但是tail又不能直接是head的引用,因为中间tail要遍历到尾部,如果是head的引用会改变head, 最后就发现头之后直接就是插入的新结点,所以我们用一个中间变量代替。
然后附上代码:
void insertFromTail(Link* &head, int v){
if (head == NULL) {
head = new Link;
head->value = 0;
head->next = NULL;
}
//创建新节点
Link* temp = new Link;
temp->value = v;
temp->next = NULL;
Link* head_temp = head;
Link* &tail = head_temp;
while (tail) {
if (tail->next == NULL) {
break;
}
tail = tail->next;
}
tail->next = temp;
}
接着是头插入:
由于没有遍历的过程,头插入显得比较简单,所以直接做一个变量的交换就好,直接附代码:
void insertFromHead(Link* &head, int v){
if (head == NULL) {
head = new Link;
head->value = 0;
head->next = NULL;
}
//创建新节点
Link* temp = new Link;
temp->value = v;
temp->next = NULL;
Link* next_temp = head->next;
head->next = temp;
temp->next = next_temp;
}
接着是查找删除元素,我做的时候是先分五种情况:
链表只有一个头部,头部匹配上
链表不只有一个头部,但是头部匹配上
链表的中间的元素匹配上
链表的尾部匹配上
没有元素匹配
考虑到这五种情况之后,逐一实现,然后发现,12可以合并,34可以合并,所以就有了如下的代码
void Delete(Link* & head, int v) {
if (head == NULL) {
cout << "No element to delete.\nThe list is already empty!\n";
return;
}
//头
if (head->value == v) {
Link* temp = head->next;
delete head;
head = temp;
return;
}
//中间
else {
Link* head_temp = head;
Link* &Mid = head_temp;
while (Mid) {
if (Mid->next && Mid->next->value == v) {
Link* mid_temp = Mid->next->next;
delete Mid->next;
Mid->next = mid_temp;
} else {
Mid = Mid->next;
}
}
return;
}
cout << "The element you want to delete is not in the list.\n";
}
基本操作如上,然后有一些特殊的变体,比如 链表翻转,循环链表,双向链表
链表翻转,其实不难,可以构建一个新的链表,遍历第一个链表的元素,然后逐个头插入就可以。
可能就是难点就是表头的操作, 我觉得这个就是见仁见智了,我是吧翻转之后,原表头还放在新链表表头,所以我就判断一下如果head为空,就直接返回,如果不为空,就跳过表头,再逐个头插入,否则会多插入一个表头,代码如下:
void reverse(Link* &head) {
if (head == NULL) {
return;
}
Link* temp_head = head->next;
Link* new_head = NULL;
while (temp_head) {
insertFromHead(new_head, temp_head->value);
temp_head = temp_head->next;
}
head = new_head;
}
循环链表:
其实思想就是把每个元素都变成中间元素,构造的时候把尾节点指向头结点,或者头结点指向的第一个元素。
双向链表:
每个结点的每个元素加一个指针指向前一个结点,应该实现起来也不难。
链表就先到这里。