算法和数据结构之链表

一、基础知识

继续造火箭。
什么是链表?顾名思义,链表就是链状的数据结构,可以想象成在日常生活中,一条铁链一环扣一环的链结在一起。每个环上存储了相关的数据。在数据结构中,组成链表的节点通常有两个以上的域,一个域用来存储数据,其它的域用来存储指向其它节点的方式(指针等)。这样通过每个节点的指向其它节点的方式形成一条数据链。
链表可以有头节点,也可以没有头节点,但一定会有尾节点(C/C++中,指向NULL的指针)。链表分为单向链表、双向链表和循环链表三类。这里以单向链表为例,双向链表和循环链表类似,只是多了一些辅助的指针,看上去有些复杂而已。

算法和数据结构之链表_第1张图片
本文主要针对单向链表展开分析。

二、实现

单向链表的建立有两种形式,即头插法和尾插法两种:
1、正常建立(尾插法)
这是最常见的建立方法,把节点一个个插入到当前节点的尾部,形成一条链。

算法和数据结构之链表_第2张图片


#include 
struct Node {
	int d;
	Node* next;
	Node(int x) : d(x), next(nullptr)
	{
	}
};
// 尾插法
Node* CreateListTail(int len) {
	if (len < 1)
	{
		return nullptr;
	}

	Node* first = new Node(100);
	Node* pCur = first;
	Node* pNode = nullptr;

	int num = 1;
	while (num <= len - 1)
	{
		pNode = new Node(num);
		pCur->next = pNode;
		pCur = pNode;
		num++;
	}

	pCur->next = nullptr;
	return first;
}


int main()
{
	int len = 5;

	Node*p = CreateListTail(len);
	for (int num = 0; num < len; num++)
	{
		std::cout << p->d << "  ";
		p = p->next;
	}

}

2、头插法

头插入的方法第一次是在阅读MFC的源码时看到的,大家可以参考稻田插秧,其实就是一种头部插入(从插秧人角度看),插秧人面向的始终是第一颗秧苗(最新插入的始终是头部)。

算法和数据结构之链表_第3张图片

struct Node {
	int d;
	Node* next;
	Node(int x) : d(x), next(nullptr)
	{
	}
};

// 头插法
Node* CreateListHead(int len) {
	if (len < 1)
	{
		return nullptr;
	}


	Node* head = new Node(100);
	head->next = nullptr;
	Node* pNode = nullptr;

	int num = 1;
	while (num <= len - 1)
	{
		pNode = new Node(num);
		pNode->next = head;
		head = pNode;
		num++;
	}

	return head;
}
int main()
{
	int len = 5;
	Node*p = CreateListHead(len);
	for (int num = 0; num < len; num++)
	{
		std::cout << p->d << "  ";
		p = p->next;
	}


}

三、应用

1、逆转的转置(逆向)
链表的转置有两种方法,即递归和就地转置。原地转置其实不复杂,主要是要理清楚三个指针的转换(所以又叫三指针法):

list* reverse(list*head)
{
	list* pre = head;
  list* cur = head->next;
	list* tmp = head->next->next;
	while (cur)
	{
		tmp = cur->next;
		cur->next = pre;
		pre=cur;
		cur = tmp;
	}

  head->next = NULL;

  return pre;
}

类似于把链表分成三个一组,每次都把当中节点的指向下一个节点的指针从原来指向后面的,改成指向前面的。同时将当中节点后移一位。
递归方法:

list* reverse(list* head)
{
    if (head == NULL || head->next == NULL)
    {
       return head;
    }
    else
    {
        list* nhead = reverse(head->next);
        head->next->next = head;
        head->next = NULL;
        return nhead;

    }
}

2、环判断
链表中有没有环的是经常遇到的问题,判断的方法就是设置两个指针,一个指针一步步的前进,另外一个指针两步两步前进,如果二者相遇,则表示链表有环。即:

bool Circle(list* node)
	{
		list* fast = node, * slow = node;
		while (fast != nullptr && fast->next != nullptr)
		{
			fast = fast->next->next;
			slow = slow->next;
			if (fast == slow)
				return true;
		}
		return false;
	}

3、入口判断
两个slow指针p1和p2,同时从交点和链表头出发,第一次相遇的位置即为入口.证明略。

4、环及非环长度
环长度是在环判断过程后,fast停止不动,slow继续前进,增加一个slow计数器,再次相遇后,计数器即为环长度。

int CircleLen(list* node)
	{
		list* fast = node, * slow = node;
		bool has = false;
		while (fast != nullptr && fast->next != nullptr)
		{
			fast = fast->next->next;
			slow = slow->next;
			if (fast == slow)
			{
				has = true;
				break;
			}
		}
		if (!has)
    {
      return 0;
    }

		int len = 0;
    
		do
		{
			len++;
			fast = fast->next->next;
			slow = slow->next;
		} while (fast != slow);
    
		return len;
	}

非环长度可以利用上面入口的判断,从链表指针头到入口即为非环的长度。

5、c++11
C++11中std::forward_list提供了一个单向链表的模板,应用更安全更便捷。但它仍然没有解决链表的根本问题,也就是随机性访问的问题,同时其出于于效率的考虑,没有提供类似于Size()的成员函数,导致其在使用时,需要进行std::distance(_begin, _end)才能得到长度大小。

四、总结

链表在面试中经常遇到的主要是转置和查找环,这是基本需要掌握的。至于链表的建立,只要记住尾部插入基本就没有问题。头部入在实际工程中经常遇到,因为头节点不需要有专门的指针来保存,头节点始终是头节点,更容易维护。至于其它的,都属于比较高的要求了,达到更好,达不到一般也不会有太大影响。
算法和数据结构之链表_第4张图片

你可能感兴趣的:(C++,AI及算法)