Dijkstra算法优化2:邻接表储存图、优先级队列 (堆) 优化(C语言实现)

在上一节中,我们用邻接表对Dijkstra算法进行优化。在这一小节,我们再加上优先级队列 (堆) 优化,使总的时间复杂度降低到O(N + M) * logN。值得注意的是,用优先级队列对Dijkstra算法进行优化时,只需要建堆、出列、向上和向下维护堆这4个功能即可。不需要入列。完整的代码如下:


#include <stdio.h>
#include <stdlib.h>

int size = 6; //一共6个点的地图 
int size2 = 6; //一共6个点的地图 
int inf = 999; //无穷大
int distance[6]; //一维数组,记录距离 
int book[6]; //标记每一个点是否是确定值 

//LinkNode是用于邻接表中,一个结点连接的其它结点 
//LinkNode只需要有结点的编号、权重即可。 
struct LinkNode{
	int index = 0;
	int weight = 0;
	struct LinkNode * next = NULL; //指向下一个结点 
};

//声明结构体ArrayNode,用于邻接表中的数组 
struct ArrayNode{
	struct LinkNode * next = NULL; //指向链表的头结点
};

//用一个数组存放首元素。每个首元素后面用指针连接各个结点 
struct ArrayNode arrNode[6]; //这是地图 
int heap[6]; //优先级队列,这里就叫堆吧。里面的内容是在arrNode中的index 
int position[6];//记录每个顶点在最小堆中的位置

void showDistance()
{
	printf("showDistance:\r\n");
	for(int i = 0; i < size2; i++)
	{
		printf("%d, ", distance[i]);
	}
	printf("\r\n\r\n");
}

//头插法添加数据,得到的结果的次序是反的
void addNode(int parentIndex, int nodeIndex, int weight)
{
	struct ArrayNode * myArrayNode;
	myArrayNode = &arrNode[parentIndex];
		
	//向操作系统申请空间 
	struct LinkNode *temp = (struct LinkNode *)(malloc(sizeof(struct LinkNode)));
	temp->index = nodeIndex;
	temp->weight = weight;
	temp->next = myArrayNode->next;
	myArrayNode->next = temp; 
}

//用map对distance数组进行初始化 
void initDistance(int startIndex)
{
	for(int i = 0; i < size; i++) 
	{
		distance[i] = inf; //初始化,默认距离为infinite 
	}
	
	distance[startIndex] = 0; //起点到自己的距离为0 
	
	struct ArrayNode * nodeInArray = &arrNode[startIndex];
	struct LinkNode *linkNode = nodeInArray->next; //找到与原点直接连接的点 
	while(linkNode != NULL)	
	{
		int index = linkNode->index; //与原点直接连接的点的index 
		distance[index] = linkNode->weight; //初始化的时候,weight就是距离 
		linkNode = linkNode->next;// 找到下一个结点 
	}
}

//Heap数组进行初始化 
void initHeap()
{
	for(int i = 0; i < size; i++) 
	{
		heap[i] = i;
		position[i] = i;
	}
}

//求优先级队列heap中的三个元素的最小值的编号
//三个参数分别是是father和两个儿子的编号。
//father一定存在,但两个儿子不一定存在。  
int getMinIndex(int father, int leftSon, int rightSon)
{
	int minIndex = father; //默认father是最小值的编号
	int minDistance = distance[heap[father]]; //最小值 
	
	//leftSon < size表示左儿子存在,且左儿子比father更小
	if(leftSon < size && minDistance > distance[heap[leftSon]])
	{
		minDistance = distance[heap[leftSon]];//更新最小值
		minIndex = leftSon; //更新最小值的编号
	} 
	if(rightSon < size && minDistance > distance[heap[rightSon]])
	{
		//minDistance = distance[heap[rightSon]]; //更新最小值
		minIndex = rightSon; //更新最小值的编号
	}
	return minIndex;
}

//交换优先级队列中的元素。这里要注意,一定要同步更新HeapNode中的indexInHeap 
void swap(int a, int b) 
{
	int temp = heap[a];
	heap[a] = heap[b];
	heap[b] = temp;
	
	//同步更新position中的数据 
	temp = position[heap[a]];
	position[heap[a]] = position[heap[b]];
	position[heap[b]] = temp;
}

//shiftDown()是部分维护堆,时间复杂度为O(logN)
//删除元素时用shiftDown(),把数据从上往下调整 
int shiftDown(int i) //调整以编号为i的元素以下的小顶堆 
{
	int minIndex = 0; //记录最小值的编号 
	while(true)
	{
		//在father和两个儿子之间找到最小值 
		minIndex = getMinIndex(i, 2 * i + 1, 2 * i + 2);
		if(minIndex != i) //如果father不是最小值 
		{
			swap(i, minIndex); //交换最小值和father 
			i = minIndex; //更新i 
		}
		else //如果father是最小值 
		{
			break; //退出循环 
		}
	}
	return i;
}

//shiftUp()是部分维护堆,时间复杂度为O(logN)
//添加元素时用siftUp(),把最底下的元素往上调整 
int shiftUp(int i)  
{
	int minIndex = 0; //记录最小值的编号 
	while(true)
	{	
		//这个i一定是儿子结点,但不知道是左儿子还是右儿子 
		//数组从0开始计数,不管i是左儿子还是右儿子,father一定是(i - 1) / 2
		int father = (i - 1) / 2; 
		
		//在father和两个儿子之间找到最小值 
		minIndex = getMinIndex(father, 2 * father + 1, 2 * father + 2);
		if(minIndex != father) //如果father不是最小值 
		{
			swap(father, minIndex); //交换最小值和father 
			i = father; //更新i,以便下一轮循环重新计算father的值 
		}
		else //如果father是最小值 
		{
			break; //退出循环 
		}
	}
	return i;
}

//建堆:建堆是最彻底的维护堆。但是时间复杂度较高,为O(N) 
void createHeap()  
{
	for(int i = size / 2; i >= 0; i--) //线性建堆 
	{
		shiftDown(i);
	}
} 

//优先级队列出列,得到最小值。
//出列后要重新维护堆,这样才能保证堆顶元素是最小值 
int dequeue()  
{
	if(size <= 0) //防止出错 
	{
		return -1; //返回-1表示出错 
	}
	int minIndex = heap[0]; //记录当前堆中的最小值的编号 
	
	//这三行就是堆排序中的一次循环 
	swap(0, size - 1);//把首元素与尾元素交换
	size--;//堆的大小-1
	shiftDown(0);//重新维护堆:向下调整数据
	 
	return minIndex; //返回最小值 
} 

//Dijkstra算法 
void Dijkstra(int startIndex)
{
	int count = 0; //确定了最小距离的点的数量
	book[startIndex] = 1; //起始位置要先标记为已经访问过了 
	createHeap(); //建堆 
	dequeue(); // 出列
	
	while(count < size2) //这样写代码,形式上与Prim算法高度一致 
	//遍历每一个点,找最小值。可以用小顶堆优化 
	//for(int i = 0; i < size2; i++)	
	{
		int minDistance = distance[heap[0]];//堆顶元素的distance最小 
		int minIndex = dequeue(); // 出列
		if(minIndex < 0)
		{
			break;
		}
		
		printf("minIndex = %d, minDistance = %d\r\n", minIndex, minDistance);
		book[minIndex] = 1; //估计值变成确定值,标记有最小值的那个点被访问过 
		count++;
		
		//更新distance数组 
		struct ArrayNode * nodeInArray = &arrNode[minIndex];
		struct LinkNode * linkNode = nodeInArray->next; //获得与最小距离的结点连接的结点 
		while(linkNode != NULL)
		{
			int index = linkNode->index;
			
			//更新距离的两个条件:没有被标记过,且距离可以变小 
			if(book[index] == 0 && distance[index] > linkNode->weight + minDistance)  
			{
				distance[index] = linkNode->weight + minDistance;
				
				//更新一个结点的值,由于更新后的值一定比原先的值小
				//所以对于小顶堆而言,一定是向上更新 
				shiftUp(index);
			}
			linkNode = linkNode->next; //下一个结点 
		}
	}	
}

//释放资源 
void releaseResource()
{
	struct LinkNode * temp; 
	for(int i = 0; i < size; i++) //把数组中每个点的相邻结点的指针全部释放掉 
	{
		temp = arrNode[i].next;
		while(temp != NULL)
		{
			temp = temp->next; //用temp指向下一个结点。 
			free(arrNode[i].next); //这时就可以free与arrNode[i]直接连接的结点 
		}
	}
}

//遍历一个结点,和它的所有相连的结点 
void traverse(int parentIndex)
{
	struct ArrayNode * myArrayNode = &arrNode[parentIndex];
	printf("%d --> ", parentIndex); //打印数组中的结点 
	struct LinkNode * myLinkNode = myArrayNode->next; //链表中的结点 
	
	while(myLinkNode != NULL) //链表中的结点不为空 
	{
		printf("%d (%d) --> ", myLinkNode->index, myLinkNode->weight);
		myLinkNode = myLinkNode->next;
	}
	printf("\r\n");
}

void traverseAll()
{
	for(int i = 0; i < size; i++) //对每个节点遍历它的相邻节点
	{
		traverse(i);
	}
}

int main()
{
	int startIndex = 0; //计算0号点到其它点的最短距离 
	
	//插入数据 
	addNode(0, 1, 10); addNode(0, 2, 16); addNode(0, 3, 14); addNode(1, 0, 10); 
	addNode(1, 3, 15); addNode(1, 4, 24); addNode(2, 0, 16); addNode(2, 3, 14); 
	addNode(2, 5, 16); addNode(3, 0, 14); addNode(3, 1, 15); addNode(3, 2, 14);
	addNode(3, 4, 23); addNode(3, 5, 8);  addNode(4, 1, 24); addNode(4, 3, 23); 
	addNode(4, 5, 22); addNode(5, 2, 16); addNode(5, 3, 8);  addNode(5, 4, 22);
	
	printf("初始状态:\r\n");
	traverseAll(); //全部遍历 
	
	initDistance(startIndex); //初始化distance数组 
	showDistance(); //显示distance数组 
	initHeap(); //初始化堆
	
	Dijkstra(startIndex);
	printf("用邻接表、优先级队列优化之后,Dijkstra算法的结果:\r\n");
	showDistance(); //显示distance数组 
	
	releaseResource(); //释放内存 
	printf("Hello\r\n"); //释放内存之后,再访问就会出错。具体表现为无法打印Hello
	return 0;
}

计算结果如下:

在这里插入图片描述上述就是用邻接表、优先级队列 (堆) 优化的Dijkstra算法。优化之后的时间复杂度为(N + M) * logN。这个算法适用于稀疏图,如果用在稠密图中,速度还不如优化前的Dijkstra算法。所以要搞清楚被研究的图是什么性质的再选择算法。另外,优化后的Dijkstra算法同样不适用于带有负权回路的图。这个算法用C语言实现比较复杂,如果用C++实现就会简单很多,原因是C++中已经实现了链表、优先级队列等算法,只需要直接调用这些算法就可以了。关于这部分内容就请小伙伴们自己上网搜索吧。

猜你喜欢

转载自blog.csdn.net/wangeil007/article/details/107509031