生活记录

先聊家常,不想看废话的请直接跳到下面看代码分析

大半个月没有更新博客了,大半个月也做了很多事情。依旧被困在英国,并且得知下个月的机票又又又被取消了;第二遍参加了某大公司的机试以及再次的失败;写了一个c++版本的单链表(主要是练习对象的各种操作);对C++更深入的学习了一遍,主要是学习了STL库吧,还有就是其余区别与C语言C++拥有的功能;再次粗略了解了一下windows.h库(window编程)和socket.h(socket编程)库,发现这真不是专门学通信与信号处理工程的人随便学得来的,还要有很扎实的计算机网络和计算机原理的基础;不停的在牛客和LeetCode刷编程题。

Leed牛大战

在这介绍一下牛客和LeetCode两大刷题网站。国内的话,刷牛客最大的好处就是有很多公司网试的真题,而且很多公司的网络机试都是在牛客平台上进行的。所以对于即将要机试的朋友,最好在牛客上多刷题。除了这个原因之外就是,因为很多时候机试是不能够用自己电脑上的IDLE,而是要用牛客上的IDLE。但是我其实真用不惯它那个网页版本的IDLE,各种调试整体来说都不太方便,所以机试前,尽量尽快的熟悉牛客网上的IDLE对于考试的时候也有帮助。牛客综合来说,最大的优点就是有大量公司的真题,而且这些公司也会从牛客上抽题来考。
接着来聊聊LeetCode,对比于牛客,LeetCode上也有很多公司真题,然而,并不是免费的,需要开通会员,如果没记错的话,会员是500元一年吧。除了这个缺点之外,LeetCode的题库还是非常的大的,有非常之多的习题。一开始在牛客刷题再转到LeedCode的话可能会有些不习惯。因为LeedCode是在规定在类或者结构体内编程的,然后系统会自动调用我们写好的类和结构运行看代码能不能实现,而牛客就是直接像我们平常编程的一样自己创建头文件,域名,main函数来写代码。好像说了很多LeedCode的不好和牛客的好,不过其实并非如此。一旦习惯了LeedCode之后,会上瘾!!!因为当你的代码通过之后,LeedCode还会提供两个指标来评估你的代码质量,一个就是代码时间,另一个就是内存消耗。而且这两个指标会分别告诉你,你的代码超越了这个平台上的百分之多少的用户。所以很多时候你写完代码之后,还会思考如何来提高代码质量,而并不仅仅把代码写出来就算了,毕竟对于新手,很多时候为了代码能通过,不会思考算法,而是直接暴力解题。当如果你想把代码质量提高时,你就会去看其他用户的分析,笔记。LeedCode在用户分享代码和笔记方面又比牛客要做的好。因为大部分的题,LeedCode都会官方提供一个视频来讲解它不同的解题方法,或者大牛用户们也能提供视频和图片,而不像在牛客上一样,只能看文字笔记而枯燥乏味。
这两个编程流量的网站,各有好坏,很多刚入门的朋友都纠结选哪个开始比较好。希望我以上的分析能给予到帮主吧。

代码分析

这是这个系列的第三篇分析了,具体目录是这样的:

  1. 单链表及各项操作介绍
  2. 单链表初始化
  3. 单链表打印(遍历),查询,定位,插入,删除,链表长度,即本文
  4. 单链表反转,排序

打印(遍历)

曾经有一次电话面试,考官问过小冯我什么是遍历,所以遍历也是个小考题吧。
遍历这个词在数据结构中频发出现,特别在之后的树结构当中,还会有前序,中序,后序遍历。那么什么是遍历呢? 遍历的意思就是把整个结构体的每一个节点都看一遍。度娘上的解释很形象。。。。遍历就是皇帝下江南,把所有风光都看一遍,但不能碰,一碰就会泄露身份,不知会不会有刁民想害朕。。。所以遍历也一样,只能读取,不能修改,而且必须从头到尾都要看一遍。
对于单链表,遍历只有一种,因为它只有一个方向,必须一个挨一个的看。对于树这种结构,在这稍微提一下,树中的节点的分支有两个,分支里头还有分支,所以看的顺序就有好几种,就是上面提到的前中后序遍历。更详细的请自行查阅。。。
所以当链表被初始化完了或者修改完之后,我们会尝试遍历一下,来把列表打印出来,看看代码执行得有没有问题。
代码如下:

void _print(node* head) {
	if (empty(head)) {
		node* print = head->next;
		while (print != NULL) {
			printf("%d,", print->value);
			print = print->next;
		}
	}
}

可以看到这里有一个empty函数,empty函数也是自己定义的,是一个返回整型的函数,用来判定链表是否为空。如果为空返回0,否则返回1。
对于遍历,我们不希望head会被修改,所以我们会先定义跟head一模一样的变量(节点)——print。 但是head的数据域里不存放任何内容,所以可以直接让print等于下一个节点,来读取数据。
我们希望把单链表的数据从头读到尾,而尾最后一个节点的指针域为NULL,所以while函数以这个标准考量。然后while里面便是,先读取当前的数据域的内容,然后使print等于下一个节点。当print等于NULL时,说明遍历结束。
empty函数:

int empty(node* head) {
	if (head->next == NULL) {
		printf("这是一个空链表。\n");
		return 0;
	}
	return 1;
}

数据查询

数据查询就是输入一个值x,然后看链表中第x位的值,x相当于数组里的索引。
先来初始化一下数据查询功能:

void f1(node** head) {
	printf("请问你想查询第几位数据: ");
	int check;
	scanf_s("%d", &check);
	node* pos = _check(*head, check);
	if (pos)
		printf("第 %d 位数据为 %d\n", check, pos->value);
}

_check函数就是实现数据查询的主要函数:

//数据查询													
node* _check(node* head, int check) {
	node* h = head;
	int j = 0;
	//检验是否为空链										
	if (!empty(head)) {
		return NULL;
	}
	//检验输入查询是否正常									
	else if (check < 1) {
		printf("请输入一个正确的方位。\n");
		return NULL;
	}
	//开始查询											
	else {
		while (h->next != NULL && j < check) {
			h = h->next;
			j++;
		}
		//如果j小于check,说明了check太大,正常期望j等于check,不存在j大于的情况
		if (j < check) {
			printf("没有你想查询的位数。链表没有这么长。\n");
			printf("这个链表的尺寸为: %d 位.\n", j);
			return NULL;
		}
		//正常输出											
		else {
			return h;
		}
	}
}

首先就是老操作,看看这个列表是否为空,除此之外还要看我们查询的值是否正常:假如输入一个0或者负数,就会直接返回NULL,因为索引必须大于0(看个人定义,有些例子可以从0开始)。
然后如果输入值没问题,就开始寻找相应位置的节点,在此前我们必须先定义一个计数j。j初始值等于0;同样的,因为我们不希望head被改变,所以直接定义一个等于head的节点来代替head执行。
查询操作也是用while来完成。条件跟打印差不多,不过要多一条就是j必须小于我们想要查询的位置。换句话说就是当j等于我们想要查询的位置的时候,这个循环就会终止。
当while loop完成之后,并不能直接返回节点读取数据。因为,跳出loop的条件有两个,随便一个不成立,loop便会结束。理想情况下和不理想的就是:

理想情况1
a.h->next != NULL **成立**
b. j < check	  **不成立**

或者

理想情况2
a.h->next == NULL **不成立**
b. j < check 	  **不成立**

所以无论a怎样,都不会影响到理想结果
1  2 = b

所以不理想情况便是:非b。
非b就是: j < check 成立

经过上面的分析可以发现,当j小于我面想要查找位置的时候,就是不理想的情况,而这种不理想的情况又是什么呢?就是我们想要查询的位置不存在,大于链表原本的范围,所以同样返回值为NULL。

数据定位

数据定位是相对于数据查询得一种操作。数据定位也是遍历的一种哦。 数距定位就是输入一个你想要的数据,然后链表一个一个的读取节点数据来做对比,然后输出他们在链表中所在的位置。跟打印不同它并不是把每一个数据都打出来,不过会返回一个计数,这个计数就是位置,在数组中叫做索引。

数据定位的准备工作:

//定位操作
void f2(node** head) {
	printf("请问你想查找的数据是: ");
	int target;
	int count = 0;
	scanf_s("%d", &target);
	printf("你想查找的数据它的位置在: ");
	if (empty(*head))		_locate(*head, target, count);
	printf("\n");
}

数据定位主函数:

//数据定位													
void _locate(node* head, int target, int i) {
	node* local = head;
	local = local->next;
	i++;
	while (local != NULL) {
		if (local->value == target)	printf("%d ", i);
		local = local->next;
		i++;
	}
}

定位操作,综合来说不难,老规矩,判断是否为空函数,然后便是while loop。 这个while loop条件跟之前的print还是一样,一直到没得读取为止。loop里面照样有一个计数器,当节点上的数据等于我们想要查询的数值时,便会返回计数器。

插入

插入的操作有点类似于查询。因为假如我们要想在第n位插入一个新节点时,我们要先找到第(n-1)位,然后把节点插入进(n-1)位之后就可以了。所以在这里,我们可以先直接仿照之前我们写的查询功能,注意,不是应用哦,因为具体情况具体分析,不能直接照搬过来。
插入新元素准备工作:

//插入新元素
void f3(node** head) {
	printf("在第几位插入数据: ");
	int n_insert;
	scanf_s("%d", &n_insert);
	printf("插入的数值为: ");
	int n_value;
	scanf_s("%d", &n_value);
	_newInsert(*head, n_insert, n_value);
	_print(*head);
	(*head)->value = (*head)->value + 1;
}

在这里需要注意的是,当插入主函数将会是一个返回整数的函数,当插入成功时,前文说过,头文件的数据域被用来了保存了单链表的长度,所以在这个准备函数里会有一个更新链表长度的操作,即更新头结点数据域。

插入新元素主函数:

//插入新元素												
int _newInsert(node* head, int n_insert, int n_value) {
	node* pre;
	int j = 0;
	pre = head;
	while (pre->next != NULL && j < n_insert - 1) {
		pre = pre->next;
		j++;
	}
	if (j != n_insert - 1) {
		printf("抱歉,我们没有查找到你想插入的方位。\n");
		return 0;
	}
	node* p = _head();
	p->next = pre->next;
	p->value = n_value;
	pre->next = p;
	return 0;
}

可以发现,在这个功能里面,我们是不会care它到底是不是一个空函数,因为即使它是个空函数,只要我们想要插的是第一位,这个功能照样能够被进行,这就是为什么说我们只能仿照但不能直接调用查询函数的原因。然后while loop就是仿照查询功能,不过条件稍微有点不同,就是之前所说的先找到第(n-1)位。除此以外这里需要说明白就代码里的node* p:

注意,这里的 node* p = _head() 并不是说p等于head。
而是通过_head()函数通过动态分配内存的方式初始化一个
新节点。因为head的初始化就是这样完成的,所以这里可以直接调用。

所以p其实就是一个new node,然后我们先定义它的指针域来保存(n-1)节点的指针域,最后就是(n-1)的指针域指向p,这个增加新元素的流程也就完成了。
增加的具体流程为下图:


删除

删除,一个跟查询功能相似,以及和插入功能相对的功能。
删除功能准备工作:

//删除元素
void f4(node** head) {
	printf("删除第几位数据: ");
	int del;
	scanf_s("%d", &del);
	_delete(*head, del);
	_print(*head);
	(*head)->value = (*head)->value - 1;
}

这里跟插入功能一样,必须要实时进行更新。
删除功能主函数:

//删除元素
int _delete(node* head, int del) {
	node* p = head;
	if (!empty(head))	return 0;
	int i = 1;
	node* q = p;
	p = p->next;
	while (p->next != NULL && i < del) {
		q = p;
		p = p->next;
		i++;
	}
	if (i != del) {
		printf("抱歉,我们没有查找到你想插入的方位。\n");
		return 0;
	}
	q->next = p->next;
	free(p);
	return 1;
}

其实这个主函数的前半部分跟查询功能几乎是一样的,对于删除功能必须要重点提醒的就是free。因为我们的节点内存空间都是动态分配的,所以当我们把它从这个链表里拆下来以后,它还是个节点,还是占据着内存空间。所以我们必须要即使的释放它,避免内存空间浪费。

链表长度

终于都到了这篇的最后一个功能了——链表长度。
在之前的插入和删除操作,我们一直都实时更新我们的链表长度。当然这样是一种做法。比较普遍的做法应该还是通过遍历的方法来数这个链表的长度是多少。所以,在数据结构中,遍历很简单,同时遍历也很重要。
打印链表长度函数:

//链表长度
void f5(node** head) {
	printf("这个链表总共有 %d 个数据。\n", (*head)->value);
}

下篇预告

算法

遍历其实就是一种算法,以上的功能都比较简单,所以算法的内容就比较粗线。下篇主要说的是单链表的反转。我之前说过,单链表的反转会在很多公司的考题里出现,而且它有两种实现方法,一种就是迭代,另一种就是递归。而递归又是算法中比较重要的一部分。其中小冯对递归有比较深入的认识就是从这个单链表的反转开始。所以下篇会重点说一下单链表反转的两种实现方式。
除此之外,还会提及到单链表排序——泡沫排序和探讨一下复杂度吧。
最后的最后,就是会介绍应用函数指针来实现功能的调用。

更多推荐

C语言之单链表打印(遍历),查询,定位,插入,删除,链表长度