要努力,但不要急。繁花锦簇,硕果累累都需要过程!
目录
前言:
数据结构是在内存中管理数据,在对数据的增删查改的时候就会使用到一些基础的数据结构包括顺序表,链表,栈,队列等,下面我们先来剖析一一剖析最基础的数据结构顺序表和链表。
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2.顺序表
1.概念:
顺序表是用物理上一段连续的地址空间依次存放数据的线性结构,一般情况下采用数组存储,在数组上完成对数据的增删查改
2.静态顺序表:
使用定长的数组存储元素:
缺点:数据存储的时候无法确定要开辟多大的内存合适
3.动态顺序表:
使用malloc,realloc,calloc等动态开辟的数组存储,合理解决了静态顺序表的缺点:
优点:可以合理的控制数组的大小,当存放数据空间不够的时候可以进行增容 ,因此通常使用动态顺序表管理数据
1.动态顺序表的功能实现:
1.初始化顺序表:
void SeqlistInit(Seqlist* ps1);
2.尾部插入数据:
void SeqlistPushBack(Seqlist* ps1,SeqDataType n)
3.尾部删除数据:
void SeqlistPopBack(Seqlist* ps1);
4.头部插入数据:
void SeqlistPushFront(Seqlist* ps1, SeqDataType n)
5.头部删除数据:
void SeqlistPopFront(Seqlist* ps1);
![]()
6.顺序表查找:
int SeqlistFind(Seqlist* ps1, SeqDataType n)
7.顺序表在pos的位置插入n:
void SeqlistInsert(Seqlist* ps1, size_t pos, SeqDataType n);
8.顺序表中删除pos位置的值:
9.修改顺序表中pos位置的值:
void SeqlistModify(Seqlist* ps1, size_t pos, SeqDataType n)
10.打印顺序表:
void SeqlistPrint(const Seqlist* ps1);
11.销毁顺序表:
2.顺序表存在的问题:
1. 中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
4.顺序表面试题:
题目1:
原地移除数组中所有的元素val,要求时间复杂度为O(N),空间复杂度为O(1 )。
解法1:动态开辟一块空间,然后将不是val的值存放到新开辟的空间,然后再拷贝到原数组中,但是这样空间复杂度为O(N)
int removeElement(int* nums, int numsSize, int val) { int* p = (int*)malloc(sizeof(int)*numsSize); int i = 0; int pos = 0; for(i=0; i<numsSize; i++) { if(nums[i] != val) { p[pos++] = nums[i]; } } for(i=0; i<pos; i++) { nums[i] = p[i]; } return pos; }
解法二:定义两个下标,如果等于val则一个下标往后走,如果不等于则将后面下标对应的值赋给前一个下标对应的值,然后两个下标同时走,最后返回前面下标就是数组的个数:
时间复杂度为O(N),空间复杂度为O(1)
int removeElement(int* nums, int numsSize, int val) { int left1 = 0; int left2 = 0; while(left2<numsSize) { if(nums[left2] == val) { left2++; } else { nums[left1] = nums[left2]; left1++; left2++; } } return left1; }
题目2:
合并数组中的重复项:
解法:定义两个下标,如果下标对应的值相等,则一个下标往后走,直到下标对应的值不相等的时候后一个下标对应的值赋值给前一个下标对应的后一个,最后返回前一个下标加一的值就是数组的个数:
int removeDuplicates(int* nums, int numsSize) { int left1 = 0; int left2 = 0; while(left2 < numsSize) { if(nums[left2] == nums[left1]) { left2++; } else { nums[++left1] = nums[left2]; left2++; } } return left1 + 1; }
题目3:
合并两个有序数组:oj链接
解法:
情况一:nums2先结束
情况二:nums1先结束:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) { int end1 = m - 1; int end2 = n - 1; int end = m + n - 1; while(end1 >= 0 && end2 >= 0) { if(nums2[end2] > nums1[end1]) { nums1[end] = nums2[end2]; end2--; end--; } else { nums1[end] = nums1[end1]; end--; end1--; } } //nums1先结束: while(end2 >= 0) { nums1[end] = nums2[end2]; end2--; end--; } }
3.链表:
1.概念:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
注:
1.链式结构在逻辑上是连续的,在物理上不一定连续;
2.现实中结点一般都是从堆上申请出来的;
3.从堆上申请的空间,是按照一定的策略来分配的可能连续,也可能不连续
2.链表的分类:
1.单向链表:
2.单向链表的实现:
1.创建结构体:
2.打印单链表:
void SListPrint(SLTNode* phead);
3.创建一个新的结点:
SLTNode* BuySLTNode(SLTDataType x);
4.链表头部插入数据:
void SListPushFront(SLTNode** pphead, SLTDataType x);
注意:要改变结构体,所以用结构体指针的地址
5.链表尾部插入数据:
void SListPushBack(SLTNode** pphead, SLTDataType x);
注意:尾部插入数据,如果链表不为空,改变结构体的成员,所以用结构体指针就行了
6.链表头部删除数据:
7.链表尾部删除数据:
void SlistPopBack(SLTNode** pphead);
![]()
8.链表中查找数据:
SLTNode* SlistFind(SLTNode* phead, SLTDataType x);
9.链表中插入数据:
void SlistInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
10.链表中删除数据:
void SlistEarse(SLTNode** pphead, SLTNode* pos);
void SlistEarseAfter(SLTNode* pos);
11.销毁链表:
void SListDestory(SLTNode** pphead);
3.链表面试题:
题目1:
1 . 删除链表中等于给定值 val 的所有结点。
解法1:定义两个指针,把是val的值free掉
struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* cur = head; struct ListNode* prev = NULL; while(cur) { if(cur->val == val) { //1.第一个结点就是val if(head->val == val) { cur = head->next; free(head); head = cur; } else { prev->next = cur->next; free(cur); cur = prev->next; } } else { prev = cur; cur = cur->next; } } return head; }
解法二:不是val的值尾插到新的结点:
struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* newhead = NULL; struct ListNode* cur = head; struct ListNode* tail = NULL;//尾指针 while(cur) { if(cur->val != val) { if(tail == NULL) { newhead = tail = cur; } else { tail->next = cur; tail = tail->next; } cur = cur->next; } else { struct ListNode* del = cur; cur = cur->next; free(del); } } if(tail) tail->next = NULL; return newhead; }
解法三:带哨兵头的链表:
特点:第一个结点不存放有效数据:
struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode)); struct ListNode* tail = guard; struct ListNode* cur = head; while(cur) { if(cur->val != val) { tail->next = cur; tail = tail->next; cur = cur->next; } else { struct ListNode* del = cur; cur = cur->next; free(del); } } if(tail) tail->next = NULL; head = guard->next; free(guard); return head; }
题目二:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有 结点组成的
解法:定义两个指针和一个带哨兵位的结点,两个结点比较,尾插到带哨兵位的结点的后边
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode* cur1 = list1; struct ListNode* cur2 = list2; struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode)); guard->next = NULL; struct ListNode* tail = guard; while(cur1 && cur2) { if(cur1->val < cur2->val) { tail->next = cur1; cur1 = cur1->next; } else { tail->next = cur2; cur2 = cur2->next; } tail = tail->next; } if(cur1) tail->next = cur1; if(cur2) tail->next = cur2; struct ListNode* head = guard->next; free(guard); return head; }
题目三:
反转一个单链表:
解法1:取结点头插到新的链表:
struct ListNode* reverseList(struct ListNode* head){ struct ListNode* newhead = NULL; struct ListNode* cur = head; while(cur) { struct ListNode* next = cur->next; cur->next = newhead; newhead = cur; cur = next; } return newhead; }
解法2:反转链表的指向:
struct ListNode* reverseList(struct ListNode* head){ struct ListNode* n1 = NULL; struct ListNode* n2 = head; struct ListNode* n3 = NULL; while(n2) { n3 = n2->next; n2->next = n1; n1 = n2; n2 = n3; } return n1; }
题目4:
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则 返回第二个中间结点。
解法1:直接遍历,求出链表的长度,然后再遍历到/2就是中间结点:
struct ListNode* middleNode(struct ListNode* head){ int len = 0; struct ListNode* cur = head; while(cur) { cur = cur->next; len++; } len /= 2; while(len--) { head = head->next; } return head; }
解法2:定义两个指针,slow和fast指针,slow指针一次走一步,fast指针一次走两步,当fast指针结束的时候,slow就是中间结点:
struct ListNode* middleNode(struct ListNode* head){ struct ListNode* slow = head; struct ListNode* fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; }
题目5:
输入一个链表,输出该链表中倒数第k个结点。
解法:先让fast指针走k步,然后再同时走
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) { // write code here struct ListNode* slow = pListHead; struct ListNode* fast = pListHead; while(k--) { //k>n if(fast == NULL) return NULL; fast = fast->next; } while(fast) { slow = slow->next; fast = fast->next; } return slow; }
题目6:
输入两个链表,找出它们的第一个公共结点。
解法:先去遍历求出两个链表的长度,然后再走差距步,最后同时走,第一个相等的交点就是公共交点:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode* curA = headA; struct ListNode* curB = headB; if(headA == NULL || headB == NULL) { return NULL; } //先找两个链表的尾结点: int lenA = 0; while(curA->next) { curA = curA->next; lenA++; } int lenB = 0; while(curB->next) { curB = curB->next; lenB++; } if(curA != curB) { return NULL; } //走差距步: struct ListNode* longlist = headA; struct ListNode* shortlist = headB; if(lenA < lenB) { longlist = headB; shortlist = headA; } int m = abs(lenA-lenB); while(m--) { longlist = longlist->next; } //同时走,找第一个相等的结点 while(longlist != shortlist) { longlist = longlist->next; shortlist = shortlist->next; } return shortlist; }
题目7:
给定一个链表,判断是否有环:
解法:定义两个指针,一个指针一次走一步,一个指针一次走两步,如果链表有环,快指针和慢指针一定会相遇:
bool hasCycle(struct ListNode *head) { struct ListNode* slow = head; struct ListNode* fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; if(fast == slow) { return true; } } return false; }
题目8:
给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL:
1.公式法证明:
思路:
struct ListNode *detectCycle(struct ListNode *head) { struct ListNode* fast = head; struct ListNode* slow = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; if(fast == slow) { struct ListNode* meet = slow; while(meet != head) { meet = meet->next; head = head->next; } return head; } } return NULL; }
2.解法二:先通过快慢指针找到相遇点,然后去切割相遇点,转换成找交点:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode* curA = headA; struct ListNode* curB = headB; if(headA == NULL || headB == NULL) { return NULL; } //先找两个链表的尾结点: int lenA = 0; while(curA->next) { curA = curA->next; lenA++; } int lenB = 0; while(curB->next) { curB = curB->next; lenB++; } if(curA != curB) { return NULL; } //走差距步: struct ListNode* longlist = headA; struct ListNode* shortlist = headB; if(lenA < lenB) { longlist = headB; shortlist = headA; } int m = abs(lenA-lenB); while(m--) { longlist = longlist->next; } //同时走,找第一个相等的结点 while(longlist != shortlist) { longlist = longlist->next; shortlist = shortlist->next; } return shortlist; } struct ListNode *detectCycle(struct ListNode *head) { struct ListNode* fast = head; struct ListNode* slow = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; if(fast == slow) { //转换相交 struct ListNode* meet = slow; struct ListNode* next = meet->next; //切割环 meet->next = NULL; struct ListNode* entryNode = getIntersectionNode(next,head); //恢复环 meet->next = next; return entryNode; } } return NULL; }
题目9:
给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点
或空结点。解法:
1.拷贝原节点,链接在原结点的后边:原结点和拷贝结点建立一个链接关系,找到原结点就可以找到拷贝结点了
2.更新每个拷贝结点的random:
if(cur->random)
copy->random = copy->random->next;
3.将拷贝结点解下来,链接成新的链表:
struct Node* copyRandomList(struct Node* head) { //1.插入copy结点: struct Node* cur = head; struct Node* copy = NULL; struct Node* next = NULL; while(cur) { //复制链接: next = cur->next; copy = (struct Node*)malloc(sizeof(struct Node)); copy->val = cur->val; cur->next = copy; copy->next = next; //迭代往后走: cur = next; } //更新拷贝结点的random: cur = head; while(cur) { copy = cur->next; if(cur->random == NULL) copy->random = NULL; else copy->random = cur->random->next; //往后走: cur = cur->next->next; } //将拷贝的结点解下来,然后链接起来: struct Node* copyHead = NULL; struct Node* copyTail = NULL; cur = head; while(cur) { copy = cur->next; next = copy->next; if(copyTail == NULL) { copyTail = copyHead = copy; } else { copyTail->next = copy; copyTail = copyTail->next; } //恢复原链表的链接: cur->next = next; //迭代: cur = next; } return copyHead; }
题目10
编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结 点之前
解法:定义两个带哨兵位的头结点,将小于x的尾插到一个链表,将大于x的尾插到另一个链表,最后将两个链表链接到一起。
class Partition { public: ListNode* partition(ListNode* pHead, int x) { // write code here struct ListNode* lessGuard,*lessTail,*graterGuard,*graterTail; lessGuard = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode)); graterGuard = graterTail = (struct ListNode*)malloc(sizeof(struct ListNode)); lessGuard->next = NULL; graterGuard->next = NULL; struct ListNode* cur = pHead; while(cur) { if(cur->val < x) { lessTail->next = cur; lessTail = lessTail->next; } else { graterTail->next = cur; graterTail = graterTail->next; } cur = cur->next; } lessTail->next = graterGuard->next; graterTail->next = NULL; pHead = lessGuard->next; free(graterGuard); free(lessGuard); return pHead; } };
题目11
判断链表是否为回文结构:oj链接
解法:先通过快慢指针找到链表的中间结点,然后逆置链表的后半部分,从头开始比较是否相等,如果是回文结构则对称:
class PalindromeList { public: struct ListNode* middleNode(struct ListNode* head){ struct ListNode* slow = head; struct ListNode* fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; } struct ListNode* reverseList(struct ListNode* head){ struct ListNode* n1 = NULL; struct ListNode* n2 = head; struct ListNode* n3 = NULL; while(n2) { n3 = n2->next; n2->next = n1; n1 = n2; n2 = n3; } return n1; } bool chkPalindrome(ListNode* A) { // write code here struct ListNode* mid = middleNode(A); struct ListNode* rmid = reverseList(mid); while(A && rmid) { if(A->val != rmid->val) return false; else { A = A->next; rmid = rmid->next; } } return true; } };
3.带头双向循环链表:
1.创建结构体:
2.初始化双向链表:
LTNode* ListInit();
3.打印双向链表:
void ListPrint(LTNode* phead);
4.双向链表尾插新的结点:
void ListPushBack(LTNode* phead, LTDataType x);
5.双向链表删除最后一个结点:
void ListPopBack(LTNode* phead);
6.双向链表头插一个新的结点:
void ListPushFront(LTNode* phead, LTDataType x);
7.双向链表头删一个结点:
void ListPopFront(LTNode* phead);
8.双向链表查找一个结点:
LTNode* ListFind(LTNode* phead, LTDataType x);
9.双向链表删除pos位置之前插入一个新的结点:
void ListInsert(LTNode* pos, LTDataType x);
10.双向链表删除pos位置处的数据:
void ListEarse(LTNode* pos);
11.销毁双向链表:
void ListDestory(LTNode* phead);
4.顺序表和链表的区别:
顺序表的优点:
1.尾插和尾删效率高
2.可以通过下标进行随机访问
.顺序表的缺点:
1.头部和中部插入删除效率低
2.扩容可能会造成空间大量浪费
链表的优点:
1.任意位置插入删除效率很高
2.按需申请释放,没有空间浪费
链表的缺点:
1.不支持随机访问
相比链表,顺序表在cpu访问数据的时候高速缓冲命中率更高:
cpu在内存中访问数据的时候,会将内存中的访问数据和相关的数据一次性放到高速缓存中,而顺序表在内存中是连续存储的,链表是随机存储,所以相对于链表顺序表在cpu访问数据的时候命中率更高
总结:以上就是关于对于数据结构顺序表和链表详细实现以及面试题的总结,希望在数据结构的学习过程中能相互促进,共同进步!