表
线性表
n个数据类型相同的元素构成的有限序列
- 空表:大小为0的表
- 首节点:存在唯一一个被称为第一个的元素
- 尾节点:存在唯一一个被称为最后一个的元素
- 前驱:前驱
- 后继:后继
对于除了首尾节点的元素,它只会有一个前驱、一个后继
顺序表实现
顺序表:一组连续内存单元存储线性表的各个元素
对于静态表,main函数调用时,应传入列表的地址(&)
建表
1 2 3 4 5 6 7 8 9 10
| #define MAXSIZE 100 typedef int ElemType;
typedef struct SeqList { ElemType data[MAXSIZE]; int length; };
|
初始化
1 2 3 4
| void initList(SeqList *L){ L->length = 0; }
|
尾插元素
1 2 3 4 5 6 7 8 9 10
| int appendElem(SeqList *L, ElemType e){ if (L->length>=MAXSIZE){ printf("已满\n"); return 0; } L->data[L->length] = e; L->length++; return 1; }
|
插入元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int insertElem(SeqList *L, ElemType e, int pos){ if (L->length>=MAXSIZE){ printf("已满\n"); return 0; } if (pos<1 || pos> L->length){ printf("位置错误\n"); return 0; } for (int i = L->length-1; i>=pos-1;i--){ L->data[i+1] = L->data[i]; } L->data[pos-1] = e; L->length++; return 1;
}
|
最坏时间复杂度
需要将插入位置后的元素都后移一位
遍历列表
1 2 3 4 5 6 7
| void listElem(SeqList *L){ for (int i = 0; i < L->length; i++){ printf("%d ", L->data[i]); } printf("\n"); }
|
删除元素
1 2 3 4 5 6 7 8 9 10 11
| int delElem(SeqList *L, int pos, ElemType *e){ *e = L->data[pos-1]; if (pos < L->length){ for (int i = pos; i<L->length; i++){ L->data[i-1] = L->data[i]; } } L->length--; return 1; }
|
该删除可以返回删除掉的元素,用e变量存储
查找元素
1 2 3 4 5 6 7 8 9
| int findElem(SeqList *L, ElemType e){ for (int i = 0; i<L->length; i++){ if (e==L->data[i]){ return i+1; } } return -1; }
|
使用动态顺序表实现
通过malloc函数在堆中动态分配内存建表
不同的地方只是在建表与初始化上
1 2 3 4 5 6 7 8 9 10 11 12 13
| typedef struct { ElemType *data; int length; }SeqList;
SeqList* initList(){ SeqList *L = (SeqList*)malloc(sizeof(SeqList)); L -> data = (ElemType*)malloc(sizeof(ElemType)*MAXSIZE); L -> length = 0; return L; }
|
此时在main函数建表时只需要调用initList函数初始化并将返回的结构体指针存入一个对应变量即可
由于此时获得的直接是地址,故对应操作时便不需要取地址了
链表实现
链表存储的特点是用一组任意的存储单元存储数据(可连续可不连续)
对每一个存储单元,除了要存储当前数据,还需要存储一个指示其直接后继的信息(地址),这两部分信息组成了一个节点,n个节点链接成了一个链表,即为线性表
节点包括两个域:
- 数据域:存储数据元素信息
- 指针域:存储直接后继存储位置
单链表
next中存储了下一个节点的地址,其中尾节点中的next指向NULL,表示到达链表尾部
初始化
1 2 3 4 5 6 7 8 9 10 11 12
| Node* initList(){ Node *head = (Node*)malloc(sizeof(Node)); head->data = 0; head->next = NULL; return head; }
int main(){ Node* test = initList(); return 0; }
|
建立一个头节点,作为线性表的开始,其数据为0,地址默认为NULL
创建链表时,只需要调用initList函数并将返回的结构体指针赋给对应变量即可
头插元素
头插元素实现了在头节点后插入数据
先让新节点的next指向原头节点指向的下一个节点
再让头节点指向的节点更改为新的节点
顺序很重要
1 2 3 4 5 6
| int insertHead(Node* L, ElemType e){ Node *p = (Node*)malloc(sizeof(Node)); p->data = e; p->next = L->next; L->next = p; }
|
尾插元素
头插元素实现了在尾节点后插入数据,我们需要先寻找尾节点
找到某节点的next==NULL时便找到了尾节点
1 2 3 4 5 6 7 8
| Node* get_tail(Node* L){ Node *p = L; while(p->next!=NULL){ p = p->next; } return p; }
|
先让先前的尾节点指向新节点
再让新节点指向NULL
1 2 3 4 5 6 7 8
| Node* insertTail(Node* tail, ElemType e){ Node* p = (Node*)malloc(sizeof(Node)); p->data = e; tail->next = p; p->next = NULL; return p; }
|
注意,尾插法每次返回尾节点,这样下次插入时就可以接着上一次的插入了,不需要再get
指定位置插入元素
首先要找到待插入位置的前一个元素,然后让新节点指向该元素的下一个元素
然后让前一个节点指向新节点的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int insertNode(Node* L, int pos, ElemType e){ Node* p = L; for(int i = 0; i<pos-1;i++){ p = p->next; if (p==NULL){ return 0; }
} Node* q = (Node*)malloc(sizeof(Node)); q->data = e; q->next = p->next; p->next = q; return 1; }
|
删除节点
找到删除节点的前驱节点p
用指针q指向要删除的节点
通过改变p的后继节点实现删除
并释放删除节点的空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int delNode(Node* L, int pos){ Node* p = L; for (int i = 0; i<pos-1; i++){ p = p->next; if (p == NULL) { return 0; } } if (p->next == NULL){ printf("位置错误\n"); } Node* q = p->next; p->next = q->next; free(q); return 1; }
|
获取链表长度
1 2 3 4 5 6 7 8 9 10
| int listLength(Node* L){ Node* p = L; int cnt = 0; while(p!=NULL){ p = p->next; cnt++; } return cnt; }
|
释放链表
把头结点后的节点都释放掉
让指针p指向头节点后的第一个节点
若p不为NULL,让q指向p的后继节点,并释放p指向的节点
让p, q指向同一个节点,循环以上操作
1 2 3 4 5 6 7 8 9 10 11 12 13
| void freeList(Node* L){ Node* p = L->next; Node* q; while(p!=NULL){ q = p->next; free(p); p = q;
} L->next = NULL;
}
|
注意
- 时间复杂度:读取数据–顺序表>链表 修改数据–链表>顺序表
- 不要忘记初始化指针变量,防止其变为野指针
- 何时使用malloc?
- 声明指向一个结构的指针不会创建该结构,而是给出足够空间容纳结构可能会使用的地址
- 使用malloc可以使系统创建一个新的结构并返回指向该结构的指针
- 故若想使用指针变量沿着一个表行进,就没必要创建新的结构,不宜使用malloc
- free的结果:指针指向的地址没变,但该位置处的数据已经无定义了(野指针)
- 若未对链表进行过删除操作,则调用malloc的次数应该等于表的大小,含表头再+1
单项循环链表
特点:表中最后一个节点的指针域指向头节点,形成一个环
终止条件:p!=L或p->next!=L
判断是否有环
快慢指针
让快指针一次走两步,慢指针一次走一步
当遇不到NULL时,若有环,则两者会一直追赶,根据概率一定会有一个时候相遇
若能相遇则说明有环,否则若碰到NULL证明没有环
注意特判只有一个元素或没有元素时,不然当前节点的指针域会赋值给空指针而报RE
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: bool hasCycle(ListNode *head) { ListNode* fast = head; ListNode* slow = head; if (fast==nullptr||fast->next==nullptr) return false; while(fast!=nullptr&&fast->next!=nullptr){ fast = fast->next->next; slow = slow->next; if (fast==slow) return true; } return false; } };
|
找到环的入口
设起点到入口距离为,入口到相遇距离为,相遇再到入口距离为,则快慢指针走的路程如下:
其中n为圈数,由于快指针的速度为慢指针的两倍,故慢指针一定会在一圈内与快指针相遇
有
化简得$ = n(y+z)-yn$至少为1
多拿出一圈来将y约掉,则
当时,即快指针转一圈与慢指针相遇的情况下,,即两者会在环的入口处相遇
当时,由于是一整圈的路程,即走完若干圈后快指针还会再走距离,最后与慢指针在环的入口处相遇
故我们可以在相遇位置设置一个指针,在起始位置设置一个指针,两者相遇处即为入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; class Solution { public: ListNode* detectCycle(ListNode *head) { int cnt[10005]; ListNode* fast = head; ListNode* slow = head; if (fast==nullptr||fast->next==nullptr) return nullptr; while(fast!=nullptr&&fast->next!=nullptr){ fast = fast->next->next; slow = slow->next; if (fast==slow){ ListNode* index1 = fast; ListNode* index2 = head; while(index1!=index2){ index1 = index1->next; index2 = index2->next; } return index1; } } return nullptr; } };
|
双向链表
特点:双向链表的节点中有两个指针域:一个指向直接后继,另一个指向直接前驱
建表
1 2 3 4 5
| typedef int ElemType; typedef struct node{ ElemType data; struct node *prev, *next; }Node;
|
初始化
与单链表相同
1 2 3 4 5 6 7
| Node* initList(){ Node *head = (Node*)malloc(sizeof(Node)); head->data = 0; head->next = NULL; return head; }
|
头插法
让新节点的prev指向头节点
让新节点的next指向头节点的next
让头节点的next节点的prev指向新节点,让头节点的next指向新节点
1 2 3 4 5 6 7 8 9 10 11 12
| int insertHead(Node* L, ElemType e){ Node* p = (Node*)malloc(sizeof(Node)); p->data = e; p->prev = L; p->next = L->next; if (L->next!=NULL){ L->next->prev = p; } L->next = p; return 1; }
|
尾插法
让新节点的prev指向尾节点
让尾节点的next指向新节点
将NULL值赋给新节点的next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Node* get_tail(Node* L){ Node *p = L; while(p->next!=NULL){ p = p->next; } return p; }
Node* insertTail(Node* tail, ElemType e){ Node* p = (Node*)malloc(sizeof(Node)); p->data = e; p->prev = tail; tail->next = p; p->next = NULL; return p; }
|
指定位置插入
寻找前置节点,让新节点prev指向前置节点
让新节点的next指向后置节点
让前置节点的next的prev指向新节点
前置节点的next指向新节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int insertNode(Node* L, int pos, ElemType e){ Node* p = L; for (int i = 0; i<pos-1; i++){ p = p->next; if (p==NULL){ return 0; } } Node* q = (Node*)malloc(sizeof(Node)); q->data = e; q->prev = p; q->next = p->next; p->next->prev = q; p->next = q; return 1; }
|
删除元素
找到要删除节点的前驱节点,用指针记录要删除的节点
改变p的后继节点以及删除节点的后继节点的前驱来删除节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int delNode(Node* L, int pos){ Node *p = L; for (int i = 0;i<pos-1;i++){ p = p->next; if (p==NULL){ return 0; } } if (p->next==NULL){ printf("要删除的位置错误"); return 0; } Node* q = p->next; p->next = q->next; q->next->prev = p; free(q); return 1; }
|
顺序表与链表