天天看點

為實習準備的資料結構(2)-- 詳盡連結清單篇

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

C連結清單

連結清單在C語言的資料結構中的地位可不低。後面很多的資料結構,特别是樹,都是基于連結清單發展的。

是以學好連結清單,後面的結構才有看的必要。

初識連結清單

連結清單是一種實體存儲單元上非連續、非順序的存儲結構,資料元素的邏輯順序是通過連結清單中的指針連結次序實作的。連結清單由一系列結點(連結清單中每一個元素稱為結點)組成,結點可以在運作時動态生成。每個結點包括兩個部分:一個是存儲資料元素的資料域,另一個是存儲下一個結點位址的指針域。 相比于線性表順序結構,操作複雜。由于不必須按順序存儲,連結清單在插入的時候可以達到O(1)的複雜度,比另一種線性表順序表快得多,但是查找一個節點或者通路特定編号的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分别是O(logn)和O(1)。

但是連結清單失去了數組随機讀取的優點,同時連結清單由于增加了結點的指針域,空間開銷比較大。

連結清單有很多種不同的類型:單向連結清單,雙向連結清單以及循環連結清單。

單連結清單

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

單連結清單實作

話不多說啊,這裡我隻想直接放代碼:

#include <stdio.h>	//初學者,C語言開手
#include <conio.h>
#include <stdlib.h>
#include <memory.h>
#include <assert.h>

//節點資料結構體
typedef struct test
{	
	char name[12];		//名字
	char pwd[8];		//密碼
	int number;			//編号
	int flag;			//區分管理者和使用者	// 	0 超級管理者 1 管理者  2 普通使用者 3 屏蔽使用者
	int money;			//僅使用者有存款,初始500
} TEST_T;

//如果不多來一個資料域,怎麼能展現出通用連結清單的優勢
typedef struct reported
{
	int amount;//交易金額
	int rflag; //交易方式	1、存款 2、取款 3、轉賬轉出 4、轉賬轉入
	int lastmoney;//餘額
	int lastmoney2;//收款者的餘額
	int number1;//付款賬戶
	int number2;//入款賬戶
	char time[12];//操作時間	
} REPORT_T;

//節點描述結構體
typedef struct point
{
	void *pData;				//指向資料域
	struct point *next;			//指向下一個節點	
} POINT_T;

POINT_T * head ;
extern POINT_T * head;           

複制

這還是個通用連結清單的頭呢!!!

//建立結點
POINT_T * creat(void *data )	//建立一個屬于結構體point的函數,
//傳入結構體test的指針便可以用以操作test變量,
{								//并傳回一個point的指針用以操作point函數
	POINT_T *p=NULL;
	
	p=(POINT_T *)malloc(sizeof(POINT_T));
	if(p==NULL)
	{
		printf("申請記憶體失敗");
		exit(-1);
	}
	memset(p,0,sizeof(POINT_T));
	
	p->pData=data;
	p->next=NULL;	//處理幹淨身後事
	return p;
}

//新增節點
void add(POINT_T * the_head,void *data )				//這裡的data不會和上面那個沖突嗎?
{
	POINT_T * pNode=the_head;							//把頭留下
	POINT_T *ls=creat(data);
	//後面再接上一個
	while (pNode->next != NULL)							//周遊連結清單,找到最後一個節點
	{
		pNode=pNode->next;
	}
	pNode->next=ls;			//ls 臨時
}

//删除節點
void del(POINT_T * the_head, int index)
{
	POINT_T *pFree=NULL;					//用來删除
	
	POINT_T *pNode=the_head;
	int flag=0;
	while (pNode->next!=NULL)
	{
		if(flag==index-1)
		{
			pFree=pNode->next;				//再指向資料域就爆了
			pNode->next=pNode->next->next;	//這裡要無縫銜接
			free(pFree->pData);				//先釋放資料
			free(pFree);					//釋放指針
			break;
		}
		pNode=pNode->next;
		flag++;	
	}	
}

//計算節點數
int Count(POINT_T * the_head)
{
	int count=0;
	POINT_T *pNode1=the_head;
	while (pNode1->next!=NULL)
	{
		pNode1=pNode1->next;
		count++;		
	}	
	return count;
}

//查找固定節點資料
POINT_T * find(POINT_T *the_head,int index)
{
	int f=0;
	POINT_T *pNode=NULL;
	int count=0;
	pNode=the_head;
	
	count=Count(the_head);
	
	if(count<index)	
		printf("find nothing");
	
	while(pNode->next!=NULL)
	{
		if(index==f)
			return pNode;
		pNode=pNode->next;
		f++;		
	}
}           

複制

尾插法

若将連結清單的左端固定,連結清單不斷向右延伸,這種建立連結清單的方法稱為尾插法。尾插法建立連結清單時,頭指針固定不動,故必須設立一個搜尋指針,向連結清單右邊延伸,則整個算法中應設立三個連結清單指針,即頭指針head、搜尋指針p2、申請單元指針pl。尾插法最先得到的是頭結點。

上面那個就是。

循環連結清單

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

我不喜歡搞的花裡胡哨,把循環連結清單單獨拿出來呢,是因為之前有幾道題給我留下了印象。

判斷連結清單是否有環

就像那電影裡的情節,男主女主在小樹林裡迷路了,到處都是樹,他們兜兜轉轉,又回到了原點。

連結清單一旦成環,沒有外力的介入是繞不出來的。

我舉個栗子:

//ListNode* reverseList(ListNode* head)
//{
//    ListNode* node_temp;
//    ListNode* new_head;
//
//    node_temp = head;
//    //周遊一個節點,就把它拿下來放到頭去
//    while (head->next != NULL)
//    {
//        //先考慮隻又兩個節點的情況 
//        head = head->next;
//        new_head = head;
//        new_head->next = node_temp;
//        node_temp = new_head;
//    }
//    return new_head;
//}           

複制

我也不知道當時是哪兒來的靈感,寫出了這麼個玩意兒。後來怎麼着了?後來卡死了呗,就擱那兒繞圈,繞不出來了。

那要這麼去判斷一個連結清單是否有環呢?

其實說簡單也簡單,快慢指針就解決了,快指針兩步走,慢指針一步走,隻要兩個指針重合了,那就說明有環,因為快指針繞回來了。

時間複雜度為線性,空間複雜度為常數。

說不簡單也不簡單,因為你去判斷一個連結清單是否有環,那頂多是在測試環節,放在釋出環節未免顯得太刻意,連代碼是否安全都不能保證。

而且,有環的話一般是運作不過的,不用測,運作逾時妥妥要考慮一下環的情況,一調試就知道了。

尋找連結清單入環點

這個就比較需要腦子了,前邊那個有手就行的。

我這個人,懶了點,來張現成的圖吧。

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

看啊,在相遇之前呢,慢指針走的距離很好求的:L1 = D+S1;

快指針走的距離:設它在相遇前繞了n圈(n>1),那麼:L2 = D+S1+n(S1+S2);

不過,還有一個等量關系,不要忽略掉,快指針的速度是慢指針的兩倍,是以:L2 = 2L1;

那麼就是:n(S1+S2)-S1 = D;

再轉換一下就是:(n-1)(S1+S2)+S2 = D;

那也就是說,在相遇時候,把一個==慢指針==放在==連結清單頭==,開始周遊,把一個==慢指針==放在==相遇點==開始轉圈,當它倆相遇的時候,就是入環點了。

其實吧,用腦子想一開始很難想出來,用手想就快多了。

環的大小就不用我多說了吧,相遇之後,定住快指針,慢指針再繞一圈,再相遇的時候就是一圈了。

雙向連結清單

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

參考單連結清單。

LeetCode 上的連結清單題

記一段曾經的問題代碼

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(NULL) {}
    
};

ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {    //傳進來的參數不為空
    ListNode* temp1 = list1;
    ListNode* temp2 = list2;
	
	//ListNode* temp3 = new ListNode(0);	這要是放這裡了問題就大了
	
    while (temp1->next != NULL && temp2!= NULL) {
        if (temp1->next->val >= temp2->val) {
            ListNode* temp3 = new ListNode(0);  //這個要放在這裡,新插入的節點一定要是新鮮的
            temp3->val = temp2->val;  //這裡要注意,對新指針指派,一定要隻指派給單指針,不要把後面一串全弄過去,會出現環狀連結清單
            temp3->next = temp1->next;
            temp1->next = temp3;
            temp2 = temp2->next;
        }
        temp1 = temp1->next;
    }
    if (temp2!= NULL) {
        temp1->next = temp2;
    }
    return list1;
}           

複制

翻轉連結清單

ListNode* reverseList(ListNode* head) 
{
    ListNode* node_temp = NULL; //這裡設為NULL
    ListNode* new_head = NULL;   //鍊棧的頭

    //周遊一個節點,就把它拿下來放到頭去
    while (head != NULL)
    {
        node_temp = head;   //先将節點取出
        //先考慮隻又兩個節點的情況 
        head = head->next;  //這個不放這裡又成環了

        node_temp->next = new_head;
	//剛開始相當于是置空的,因為前面并沒有配置設定空間,而是NULL
        new_head= node_temp;
    }
    return new_head;
}               

複制

不好了解啊,這裡要明确幾點:

1、head是目前周遊的節點,可以看成疊代器。

2、new_head= node_temp,但是node_temp的變化不影響new_head。

旋轉連結清單

這個也比較有意思啊,題目時這樣的:給定一個當連結清單,讓你順時針/逆時針旋轉N個位置,==要求原地旋轉==。

我講一下思路吧:

1、将連結清單自成環。

2、從剛剛的頭往後周遊N個位置,N為要旋轉的數。

3、環斷開。

解決。

秀吧,我就是覺得解法好玩,就收藏了。

STL 中的 List

每一個自己寫過連結清單的人都知道,連結清單的節點和連結清單本身是分開設計的。

那我們來看一下List的節點設計:

template <typename T>
struct __list_node
{
	typedef void* void_pointer;
	void_pointer prev;
	void_pointer neet;
	T date;
}           

複制

顯而易見,這是一個通用雙向連結清單的節點(如果對通用連結清單不了解,建議一定要自己動手設計一個)。

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述

3、List基本函數使用

  1. 創#include<list>

    typedef struct rect

    {

    ···

    }Rect;

    list<Rect>test; //聲明一個連結清單,用于存放結構體資料

    //如果想以其他方法初始化list清單,可以移步到下一行那個Vector的介紹

  1. 增Rect a;

    ···

    test.push_back(a);

    test.push_front(a);

    //頭尾插入(雙向連結清單)

    //定點插入

    test.insert(test.begin()+10,a); //在第十個節點之前插入aundefined//删除test頭部的元素

    test.erase(test.begin());

    //删除test從頭到尾的元素

    test.erase(test.begin(), test.end());

    test.pop_back();

    test.pop_front();其實增删還是推薦使用疊代器來,因為直接對資料進行操作會存在一定的危險。

    在本文第三部分詳細講述了List疊代器操作增删。

除了這個函數:

test.clear();

這個函數安全得很,反正都清理掉了。

  1. 查、改
//疊代器
list<int>::iterator p;
for (p = test.begin(); p != test.end(); p++)
	cout << *p << " ";           

複制

要周遊還是首推疊代器。所有和周遊有關的還是用疊代器。

#include<algorithm>
sort(test.begin(),test.end());	//從頭到尾,預設從小到大排序

//一般排序都是要自定義排序的:
bool Comp(const int &a,const int &b)
{
    return a>b;
}
sort(test.begin(),test.end(),Comp);	//這樣就降序排序。            

複制

  1. 大小
test.size();	//容器已存入資料量
test.capacity();	//容器還能存多少資料量

//其實不用擔心容器不夠大,容量要滿的時候它會自己擴容           

複制

  1. 其他

    (1)壓縮list

//去除重複的元素至隻保留一個副本
test.unique();
//已經過大小排序的list才能使用           

複制

(2)合并list           

複制

test.splice(test.end(),test2);//将test2剪切到test後面           

複制

最後還是要提一下頭檔案:

分不清楚就都寫上

#include<algorithm>
#include<list>           

複制

為實習準備的資料結構(2)-- 詳盡連結清單篇

在這裡插入圖檔描述