数据结构与算法-单链表篇

        在数据结构中,链表可能是程序员会建立的最简单的数据结构。也是非常基础的一种数据结构,往往简单的东西越需要扎实,大多数的情况是眼高手低觉得很简单差不多,一写代码就出错,一运行就崩溃,完成链表这一数据结构主要在于对指针的使用。

        链表是一种线性的数据结构,相比静态申请数组空间必须要给定长度,链表更加长灵活,如果数据的个数会随着时间变化而增加或者减少,链表是一种很好的存储方式。但是缺点在于访问元素速度不能通过下标直接获得。一个完备的链表应具备:初始化链表、销毁链表,遍历打印链表,插入数据建立链表,删除数据,查找数据,这几个基础功能。熟练掌握这些基础功能之外还可以增加:反转链表,合并两个有序链表,链表去重,去掉链表所有重复元素,删除链表倒数第n个节点等。

        一个链表节点应包含数据域(void *类型最合适),和指针域(指向下一个节点的指针)。

typedef struct LinkList {
	DataType data;
	struct LinkList *next;
}LinkList;

        拿带头节点单链表来说,带头节点链表相比不带头节点链表更简单一点,也容易理解。单链表中一个节点的访问得找到它的前驱节点指针来得到,第一个节点也需要一个前驱节点指针来得到它,就在第一个节点前面放一个节点叫做头(用H表示),只用H的next指针得到值为15这个节点,H的数据域不存放数据,这就是头结点的作用所在。

1、链表的初始化

        链表的初始化就是给头结点申请一份空间,注意这里函数参数传递的是指向指针的指针,才能把在函数里面分配的空间,出函数之后真正的分配给头结点指针,如果只传一层指针,那么head只做一个形参,出函数之后并不能真正的将空间给链表头结点,就是关于形参实参的基础知识。

 

void initLinkList(LinkList **head) {
	if((*head) != NULL) {
//表示该链表已经被初始化过了不需要生成头结点
		return;
	}

	*head = (LinkList *)malloc(sizeof(LinkList));
	(*head)->next = NULL;
}

2、链表的销毁

        链表所有节点包括头结点都是动态申请的堆空间,使用完毕后必须手动释放,这里的销毁要把所有的节点空间全部释放。

        第一种销毁链表的方式是:从链表头开始遍历,也就是从前向后逐个释放每一个节点的空间,在释放下一个节点之前得先把当前的节点保存下来,再把这个节点释放掉,如果先释放当前结点,就没法获得下一个节点,好比有一座桥现在要拆掉,桥是一段段连起来的,从一头开始拆,你是一段段的去拆,你把脚下的直接拆了怎么过去另一端,先得过去另一端回头拆就不捞。

空间释放完记得把指针指向空,指针指向的空间已经没了被释放了。

 

void destoryLinkList(LinkList **head) {
	LinkList *header = *head;
	LinkList *p = NULL;

	while(NULL != header) {
		p = header;			//保存当前结点
		header = p->next;   //让当前头结点指向下一个节点
		free(p);			//把保存的节点释放掉
	}
	free(header);			
	head = NULL;
}

第二种销毁链表的方式是:从链表尾部向头部开始销毁,就不用临时保存。用递归遍历到最后一个结点,逐层向上返回,销毁每一个节点,顺序就是从头尾向头结点的顺序销毁。

 

//递归的销毁方式
void destoryLinkList(LinkList **head) {
	if(NULL == *head) {
		return;
	}

	destoryLinkList(&((*head)->next));
	free(*head);
	*head = NULL;
}


2、建立链表

        链表已经初始化了,就需要往链表中插入数据了,分为尾插法建立,和头插法建立,头插法建立的链表,链表中的数据顺序和实际输入顺序相反,尾插法则相同。

头插法:每次在头结点H的后面插入一个输入的数据,插入的过程主要是:先申请一个新的结点,链表不像数组一次性分配指定长度的空间,链表是需要增长一个就再申请一份,然后链接起来。申请完了之后给节点赋值,让新申请的节点指向头结点的next,也就是node->next = h->next,再让头结点指向这个新节点,H->next = node就完成插入操作。

 

void insertDataByHead(LinkList *head) {
	int x;
	LinkList *node;

	printf("input the data (-1 stop): ");
	scanf("%d", &x);
	while(x != -1) {
		node = (LinkList *)malloc(sizeof(LinkList));
		node->data = x;
		node->next = head->next;
		head->next = node;
		printf("input the data (-1 stop): ");
		scanf("%d", &x);
	}
}

尾插法:每次插入新的数据在链表的尾部插入就行,先找到链表的尾节点H->next == NULL,就是最后一个节点,同样插入就行。相比头插法,尾插法插入数据的时候如果链表不是一条空链表,得遍历先找到尾节点。

 

 

void insertDataByTail(LinkList *head) {
	int x;
	LinkList *node;
	LinkList *remove;

	while(NULL != head->next) {
//如果链表里面已经有数据了需要找到链表中最后一个节点
		head = head->next;
	}
	remove = head;

	printf("input the data (-1 stop): ");
	scanf("%d", &x);
	while(x != -1) {
		node = (LinkList *)malloc(sizeof(LinkList));
		node->data = x;
		node->next = remove->next;
		remove->next = node;
		remove = node;
		printf("input the data (-1 stop): ");
		scanf("%d", &x);
	}

}

3、打印链表

        给循环遍历,就行,停止条件是当前结点变为NULL就完了

void showListInfor(LinkList *head) {
	head = head->next;

	while(NULL != head) {
		printf("%d ", head->data);
		head = head->next;
	}
	printf("\n");
}


3、删除数据

        要删除链表中第index个节点p,根据index去遍历找到链表里第index个p,还需要先找到这个节点p的前驱节点q,然后让q指向要删除节点的next,把p的节点空间释放掉就完成删除一个节点,做到真正的在内存上删除掉节点。链表的删除比起数组删除要简单的多,实质上就是改变节点之间的链接关系达到删除的目的,数组则要涉及元素的大量移动。简单的说就是:要删除一个目标节点,只需设置目标前一个节点的链接指向目标节点的下一个单元。

 

int deleteElementByIndex(LinkList *head, int deleteIndex) {
	LinkList *p;
	LinkList *tmp;

	p = indexOf(head, deleteIndex);

	if(NULL == p) {
		printf("ERROR delete Index !");
		return false;
	}

//找到要删除节点p的前驱节点 如果要删除第一个节点 说明它的前驱节点就是头结点
	tmp = deleteIndex == 1 ? head : indexOf(head, deleteIndex - 1);
	tmp->next = p->next;
	free(p);

	return true;
}

LinkList *indexOf(LinkList *head, int index) {
	LinkList *p;
	int j;

	for(j = 1, p = head->next; NULL != p && j < index; j++, p = p->next);

	return j == index ? p : NULL;
}

4、在链表的第i个位置上插入一个新元素

在链表中指定的位置index后面插入一个新元素,同样是根据index先找到插入的位置节点p,给插入的节点newNode申请空间赋值。

让node->next = p->next  ,   p->next = node,插入的两行代码就是简单的尾插法逻辑。

boolean insertElement(LinkList *head, int index, DataType data) {
	LinkList *p;
	LinkList *newNode;

	p = indexOf(head, index);

	if(NULL == p) {
		printf("Error insert index !\n");
		return false;
	}

	newNode = (LinkList *)malloc(sizeof(LinkList));
	newNode->data = data;
	newNode->next = p->next;
	p->next = newNode;

	return true;
}

5、按值查找节点,求表长

从链表的第一个节点找判断当前结点的数据是否和目标数据相同,若是返回该节点指针,否则就继续向后找,直到表尾,找到链表尾还没找到那就返回的是表尾的指向NULL。

 

LinkList *searchNodeByData(LinkList *head, DataType data) {
	LinkList *p;

	for(p = head; NULL != p && p->data != data; p = p->next);

	return p;
}

        求表长就从链表头开始向后遍历,循环停止条件是head->next == null,说明已是链表最后一个有效数据节点。

int getLinkListLength(LinkList *head) {
	int count = 0;

	while(head->next) {
		head = head->next;
		count++;
	}

	return count;
}

以上的几点是一个链表基础的功能。

1、删除链表中倒数第n个节点

单链表是:H->15->23->31->21->null,给定n = 2,倒数第2个是31,删完的链表就是H->15->23-21->null。

笨办法就是先遍历一遍得到链表的长度length,然后删除正数第length - n + 1个节点。也可以达到要求,但是需要两次遍历链表。

//笨办法删除 先得到链表长度
void RemoveNthNodeFromEndofList(LinkList *head, int n) {
	int length = getLinkListLength(head);
	int index = length - n + 1;
	int i;
	LinkList *temp;

	for(i = 0; i < index - 1; i++) {
		head = head->next;
	}
	temp = head->next;
	head->next = temp->next;
	free(temp);
}

第二个方法是快慢指针的方法,给两个指针分别为fast,slow,开始的时候都指向头结点,第一次循环让fast走n步,第二次循环知道fast走到链表尾,同时slow开始从头走,循环结束之后slow就是要删除的倒数第n个节点的前驱节点。

//Linus Torvalds 的双重指针方法 
//先让一个指针fast走n步,在第二循环中让另外一个指针slow从头走,走了n步指针fast接着走直到链表尾
//这时slow走到的地方就是要删除节点的前驱节点 多帅
void RemoveNthNodeFromEndofList_2(LinkList *head, int n) {
	LinkList *fast = head;
	LinkList *slow = head;
	LinkList *temp;

	while(n-- > 0) {
		fast = fast->next;
	}

	while(fast->next) {
		fast = fast->next;
		slow = slow->next;
	}

	temp = slow->next;
	slow->next = temp->next;
	free(temp);
}


2、反转链表

介绍两种反转链表的方法:

1、逐个把链表的数据以头插法插入到头结点的后面,比较容易理解。

 

void reverseList_2(LinkList *head) {
	LinkList *p;
	LinkList *tmp;

	p = head->next;		//保存链表第一个有效数据
	head->next = NULL;	//断开头结点和链表第一数据的链接
	while(p) {
		tmp  = p;
		p = p->next;
		tmp->next = head->next;
		head->next = tmp;
	}
}		//保存链表第一个有效数据
	head->next = NULL;	//断开头结点和链表第一数据的链接
	while(p) {
		tmp  = p;
		p = p->next;
		tmp->next = head->next;
		head->next = tmp;
	}
}

2、直接改变链表各个节点之间的指向关系,多申请一个指针变量q,用来表示当前的头节点。

 

void reverseList_1(LinkList *head) {
	LinkList *p;
	LinkList *q;
	LinkList *tmp;

	p = head->next;				//保留链表第一个节点
	head->next = NULL;			//断开头结点和链表第一个节点的链接
	q = NULL;
	while(NULL !=  p) {
		tmp = p->next;
		p->next = q;
		q = p;
		p = tmp;
	}

	head->next = q;     //把链表的头结点加上去
}
			//断开头结点和链表第一个节点的链接
	q = NULL;
	while(NULL !=  p) {
		tmp = p->next;
		p->next = q;
		q = p;
		p = tmp;
	}

	head->next = q;     //把链表的头结点加上去
}

3、合并两个有序链表

有两个单链表为:H1->1->2->4->9->null, H2->2->3->4->6->8->10->null, 合并成H->1->2->2->3->4->6->8->9->10->null,循环开始遍历比较两个链表的值大小,把小的那个放进来目标链表,然后移动链表,循环停止的条件是,只要有一个链表遍历完了就停止,并在循环完了判断哪个链表还没完,接着指向它。

LinkList *mergeList(LinkList *list_A, LinkList *list_B) {
	LinkList *result_list;
	LinkList *head;

	initLinkList(&result_list);
	head = result_list;
	list_A = list_A->next;
	list_B = list_B->next;
	for(; list_A && list_B; result_list = result_list->next) {
		if(list_A->data < list_B->data) {
			result_list->next = list_A;
			list_A = list_A->next;
		}else {
			result_list->next = list_B;
			list_B = list_B->next;
		}
	}
	if(list_A) {
		result_list->next = list_A;
	}if(list_B) {
		result_list->next = list_B;
	}

	return head;
}

    这样子写虽然可以把两个有序链表合并成一个有序链表,但是在合并的时候没有申请新空间,result_list用的是list_A和list_B的空间,合并完了之后会改变A,B链表的之间的指向关系和值。应该继续改进加上申请新空间的代码。

4、链表去掉所有重复元素

有单链表H->1->2->2->3->4->4->4->5->5->6->null,去掉所有重复元素得到,H->1->3->6->null,主要使用双指针的思想去删除链表里面重复的元素,需要考虑的一点就是何时停止,边界情况的考虑,如果链表就是个H->2->2->2->null,这种情况的出现。

 

void deleteAllDuplicateEle(LinkList *head) {
	LinkList *p = head;
	LinkList *temp;
	LinkList *q = p->next;

	q = p->next;
	while(p->next && q->next && q) {
		if(q->data == q->next->data) {
			int value = q->data;
			while(q && value == q->data) {
				temp = q;
				q = p->next = q->next;
				free(temp);
			}
		}else {
			p = p->next;
			q = p->next;
		}
	}
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值