当前位置: 首页 > news >正文

宁夏网站建设优化/外包公司值得去吗

宁夏网站建设优化,外包公司值得去吗,电子商务个人网站可以备案吗,最火爆的视频制作app文章目录线性表顺序表顺序表与数组本质优点缺陷解决办法SeqList.hTest.c实现初始化与销毁扩容尾插头插打印尾删头删查找中间插入/删除修改链表优点缺点单链表逻辑结构&物理结构链表组合实现定义创建节点打印查找尾删头删头插尾插pos后插入pos前插入pos后擦除双向链表头文件…

文章目录

  • 线性表
    • 顺序表
      • 顺序表与数组
      • 本质
      • 优点
      • 缺陷
      • 解决办法
      • SeqList.h
      • Test.c
      • 实现
        • 初始化与销毁
        • 扩容
        • 尾插头插
        • 打印
        • 尾删头删
        • 查找
        • 中间插入/删除
        • 修改
    • 链表
      • 优点
      • 缺点
      • 单链表
        • 逻辑结构&物理结构
        • 链表组合
        • 实现
          • 定义
          • 创建节点
          • 打印
          • 查找
          • 尾删
          • 头删
          • 头插
          • 尾插
          • pos后插入
          • pos前插入
          • pos后擦除
      • 双向链表
        • 头文件
        • 初始化
        • 创建节点
        • 尾插
        • 头插
        • 尾删
        • 头删
        • 查找
        • 插入
        • 擦除
        • 是否为空
        • 计算大小
        • 摧毁
    • 练习
      • [27. 移除元素](https://leetcode-cn.com/problems/remove-element/)
      • [26. 删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/)
      • [989. 数组形式的整数加法](https://leetcode-cn.com/problems/add-to-array-form-of-integer/)
      • [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/)
      • [189. 轮转数组](https://leetcode-cn.com/problems/rotate-array/)
      • [203. 移除链表元素](https://leetcode-cn.com/problems/remove-linked-list-elements/)
      • [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/)
      • [876. 链表的中间结点](https://leetcode-cn.com/problems/middle-of-the-linked-list/)
      • [剑指 Offer 22. 链表中倒数第k个节点](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/)
      • [21. 合并两个有序链表](https://leetcode-cn.com/problems/merge-two-sorted-lists/)
      • 链表分割
      • VS下调试链表的题目
      • 链表的回文结构
      • [160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/)
      • [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
      • [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)
      • [138. 复制带随机指针的链表](https://leetcode-cn.com/problems/copy-list-with-random-pointer/)
      • [147. 对链表进行插入排序](https://leetcode-cn.com/problems/insertion-sort-list/)
      • 删除链表中重复的结点

线性表

常见的线性表:顺序表,链表,栈,队列,字符串
线性表在逻辑上是线性结构,(连续的一条直线)
但是在物理结构不一定是连续的
逻辑结构
物理结构

顺序表

顺序表与数组

由于顺序表结构的底层实现借助的就是数组,因此对于初学者来说,可以把顺序表完全等价为数组,但实则不是这样。数据结构是研究数据存储方式的一门学科,它囊括的都是各种存储结构,而数组只是各种编程语言中的基本数据类型,并不属于数据结构的范畴。

本质

顺序表:本质就是数组,动态增长,并且要求里面存储的数据必须是从左往右连续的
数组可以只存1 3 5位置,顺序表不行
数组是静态的

利用malloc和realloc

优点

  1. 按下标进行随机访问
  2. 空间地址连续,CPU高速缓存命中率比较高

CPU访问数据时,数据在缓存,命中;
不在缓存,不命中
假设不命中时,一次性加载16字节

image-20220210201850655

缺陷

  1. 动态增容有性能消耗,通常伴随着空间浪费
  2. 头部或中间插入/删除数据,需要挪动数据,效率比较低

解决办法

链表

image-20220205114740805

SeqList.h

#pragma once
#define N 100
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<errno.h>
#include<string.h>静态顺序表(不推荐)
//struct SeqList
//{
//	int a[N];
//	int size;
//};typedef int SeqDataType;//利用typedef插入任意类型数据typedef struct SeqList
{SeqDataType* a;//指向动态开辟的数组int size;//有效数据个数int capacity;//容量
}SeqList,SEQ;//初始化
void SeqListInit(SeqList* pseq);
//销毁
void SeqListDestory(SeqList* pseq);
//打印
void SeqListPrint(SeqList* pseq);//尾插
void SeqListPushBack(SeqList* pseq, SeqDataType x);
//头插
void SeqListPushFront(SeqList* pseq, SeqDataType x);
//尾删
void SeqListPopBack(SeqList* pseq);
//头删
void SeqListPopFront(SeqList* pseq);
//查找
int SeqListFind(SeqList* pseq,SeqDataType x);
//中间插入
void SeqListInsert(SeqList* pseq, int pos, SeqDataType x);
//中间擦除
void SeqListErase(SeqList* pseq, int pos);
//修改
void SeqListModify(SeqList* pseq, int pos, SeqDataType x);

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"void TestSeqList()
{SeqList s;SeqListInit(&s);//必须传址SeqListPushBack(&s, 1);SeqListPushBack(&s, 2);SeqListPushBack(&s, 3);SeqListPushBack(&s, 4);SeqListPushBack(&s, 5);SeqListPushFront(&s, 0);SeqListPushFront(&s, 0);SeqListPushFront(&s, 0);SeqListPrint(&s);SeqListPopBack(&s);SeqListPrint(&s);SeqListPopBack(&s);SeqListPrint(&s);SeqListPopFront(&s);SeqListPrint(&s);SeqListPopFront(&s);SeqListPrint(&s);SeqListDestory(&s);
}void TestSeqList2()
{SeqList s;SeqListInit(&s);SeqListPushBack(&s, 1);SeqListPushBack(&s, 2);SeqListPushBack(&s, 3);SeqListPushBack(&s, 4);SeqListPushBack(&s, 5);SeqListPrint(&s);SeqListInsert(&s, 0, 30);SeqListPrint(&s);SeqListErase(&s, 0);SeqListPrint(&s);SeqListModify(&s, 0, -1);SeqListPrint(&s);SeqListDestory(&s);}
int main()
{TestSeqList2();return 0;
}

实现

Tips:不要先写菜单,把接口都调正确了再写菜单

//静态顺序表(不推荐)
struct SeqList
{int a[N];int size;
};

初始化与销毁

//初始化
void SeqListInit(SeqList* pseq)
{assert(pseq);pseq->a = NULL;pseq->capacity = 0;pseq->size = 0;
}
//销毁
void SeqListDestory(SeqList* pseq)
{assert(pseq);free(pseq->a);pseq->a = NULL;pseq->capacity = pseq->size = 0;
}

扩容

void SeqCheckCapacity(SeqList* pseq)
{//满了需要增容if (pseq->size == pseq->capacity){//一开始要赋予初识空间int newcapacity = pseq->capacity == 0 ? 4 : pseq->capacity * 2;//通常增加2倍SeqDataType* newA = realloc(pseq->a, sizeof(SeqDataType) * newcapacity);/*if (newA == NULL){printf("SeqListPushBack::%s\n", strerror(errno));}*///或者if (newA == NULL){printf("realloc fail\n");exit(-1);}pseq->a = newA;pseq->capacity = newcapacity;}
}

尾插头插

//尾插
void SeqListPushBack(SeqList* pseq, SeqDataType x)
{/*assert(pseq);SeqCheckCapacity(pseq);pseq->a[pseq->size] = x;pseq->size++;*/SeqListInsert(pseq, pseq->size, x);
}//头插需要从后往前挪动
void SeqListPushFront(SeqList* pseq, SeqDataType x)
{/*assert(pseq);SeqCheckCapacity(pseq);int end = pseq->size - 1;while (end >= 0){pseq->a[end + 1] = pseq->a[end];--end;}pseq->a[0] = x;pseq->size++;*/SeqListInsert(pseq, 0, x);
}

打印

void SeqListPrint(SeqList* pseq)
{assert(pseq);for (int i = 0; i < pseq->size; i++){printf("%d ", pseq->a[i]);}printf("\n");
}

尾删头删

//尾删
void SeqListPopBack(SeqList* pseq)
{/*assert(pseq);assert(pseq->size > 0);--pseq->size;*/SeqListErase(pseq, pseq->size - 1);
}
//头删
void SeqListPopFront(SeqList* pseq)
{/*assert(pseq);assert(pseq->size > 0);//必须从前往后挪动int begin = 0;//挪到begin == size-1就停止while (begin < pseq->size - 1){pseq->a[begin] = pseq->a[begin+1];begin++;}pseq->size--;*/SeqListErase(pseq, 0);
}

查找

int SeqListFind(SeqList* pseq, SeqDataType x)
{assert(pseq);for (int i = 0; i < pseq->size; i++){if (pseq->a[i] == x){return i;}return -1;}
}

中间插入/删除

image-20220205182049310

//中间插入
void SeqListInsert(SeqList* pseq, int pos, SeqDataType x)
{assert(pseq);assert(pos >= 0 && pos <= pseq->size);//=size时表示的就是尾插SeqCheckCapacity(pseq);//从后往前挪动int end = pseq->size - 1;while (end>=pos){pseq->a[end + 1] = pseq->a[end];--end;}pseq->a[pos] = x;pseq->size++;
}
//中间擦除
void SeqListErase(SeqList* pseq, int pos)
{assert(pseq);assert(pos >= 0 && pos < pseq->size);//不能=size,=size时没有元素的int begin = pos;while (begin <= pseq->size-1){pseq->a[begin] = pseq->a[begin + 1];++begin;}pseq->size--;
}

修改

void SeqListModify(SeqList* pseq, int pos, SeqDataType x)
{assert(pseq);assert(pos >= 0 && pos < pseq->size);pseq->a[pos] = x;
}

链表

优点

  1. 按需申请内存,不存在空间浪费
  2. 任意位置O(1)时间插入删除数据

缺点

  1. 不支持下标的随机访问

总结:顺序表和链表相辅相成,使用要看具体应用场景

单链表

struct SListNode
{int data;struct SListNode* next;
};
void Test()
{struct SListNode* node1 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node2 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node3 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node4 = (struct SListNode*)malloc(sizeof(struct SListNode));printf("%p\n", node1);printf("%p\n", node2);printf("%p\n", node3);printf("%p\n", node4);
}

image-20220206204249510

地址均不连续,堆上使用的空间地址由高到低

逻辑结构&物理结构

phead(头结点)是指针变量,存的是第一个节点的地址

image-20220206204508339

image-20220206202736956

链表组合

  1. 单向双向
  2. 带头不带头
  3. 循环非循环

最多有8种

image-20220206204815192

实现

定义
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;
创建节点
//创建结点
SLTNode* BuySLTNode(SLTDataType x) {SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//开辟失败的情况if (node == NULL) {printf("BuySLTNode Failed");return;}node->data = x;node->next = NULL;return node;
}
打印
void SListPrint(SLTNode* plist) {SLTNode* cur = plist;while (cur != NULL){printf("%d->", cur->data); cur = cur->next;//cur->next里面存的就是下一个结点的地址}printf("NULL\n");
}
查找
//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x) {SLTNode* cur = plist;//不要轻易zplist//while(cur != NULL)while (cur) {if (cur->data == x) {return cur;//查找兼具修改的作用}cur = cur->next;}return NULL;
}
尾删

多个节点时,删除尾节点需要把prev->next置为NULL

image-20220206230042773

void SListPopBack(SLTNode** pplist) {//1.没有节点//2.一个节点//3.多个节点if (*pplist == NULL) {return;}else if ((*pplist)->next == NULL) {free(*pplist);*pplist = NULL;//这样就达到了删除一个节点的目的}//多个节点else {SLTNode* prev = NULL;SLTNode* tail = *pplist;while (tail->next != NULL) {prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行}
}

单个节点或NULL时需单独考虑

image-20220206230209908

头删

image-20220207002901651

void SListPopFront(SLTNode** pplist) {//1.没有节点//2.一个节点//3.多个节点if (*pplist == NULL) {return;}else {//恰好适用一个节点和多个节点的情况//保存plist->next,如果直接free plist就找不到后面的空间了SLTNode* next = (*pplist)->next;free(*pplist);*pplist = next;//因为要修改plist必须传址调用}
}
头插

image-20220206230614981

void SListPushFront(SLTNode** pplist, SLTDataType x) {//即使传进来的是NULL也能解决SLTNode* newnode = BuySLTNode(x);newnode->next = *pplist;//*pplist 其实就是plist*pplist = newnode;
}
尾插
//传址调用才行,二级指针
void SListPushBack(SLTNode** pplist, SLTDataType x) {SLTNode* newnode = BuySLTNode(x);//考虑空链表的情况if (*pplist == NULL) {*pplist = newnode;//传进来空链表,要修改plist必须传址调用}else {//找尾插地址SLTNode* tail = *pplist;//不能直接修改plist,plist一改就找不到链表了while (tail->next != NULL) {tail = tail->next;}//出来时tail->next 指向的是NULLtail->next = newnode;}
}

带上头节点可以不用传址调用,因为不需要修改plist

pos后插入
//在pos后面插入
void SListInserAfter(SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* newnode = BuySLTNode(x);//注意顺序不要反了newnode->next = pos->next;pos->next = newnode;
}//反了需要将pos->nxet提前保存起来
//在pos后面插入
/* void SListInserAfter(SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* newnode = BuySLTNode(x);SLTNode* next = pos->next;pos->next = newnode;newnode->next = next;
}
*/
pos前插入
//在pos之前插入
void SListInserBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* newnode = BuySLTNode(x);//如果pos在第一个呢?if (pos == *pplist) {//相当于头插newnode->next = pos;*pplist = newnode;}else {SLTNode* prev = NULL;SLTNode* cur = *pplist;while (cur != pos) {prev = cur;cur = cur->next;}//避免prev为空if (prev == NULL) {return;}prev->next = newnode;newnode->next = pos;}
}

在一个无头(不告诉头指针)单链表的某一个节点前面插入一个值x,怎么插?

后插,然后交换data

image-20220207001858221

pos后擦除
//在pos之后擦除
void SListEraseAfter(SLTNode* pos) {assert(pos);//pos之后无节点if (pos->next == NULL) {return;}else {SLTNode* next = pos->next;pos->next = next->next;free(next);next = NULL;}
}

image-20220207230759923

双向链表

结构虽然复杂,但是操作反而简单,这是其结构优势

头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDataType data;
}ListNode;ListNode* ListInit();
void ListPushBack(ListNode* phead, LTDataType x);
void ListPrint(ListNode* phead);
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
ListNode* ListFind(ListNode* phead, LTDataType x);
void LIstInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos);
int ListEmpty(ListNode* phead);//空返回1,非空返回0
int ListSize(ListNode* phead);
void ListDestroy(ListNode* phead);

初始化

ListNode* ListInit()
{ListNode* phead = BuyListNode(0);//指向自己,方便插入第一个节点phead->next = phead;phead->prev = phead;return phead;
}

创建节点

ListNode* BuyListNode(LTDataType x)
{ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL){printf("BuyListNode Failed\n");exit(-1);}node->next = NULL;node->prev = NULL;node->data = x;return node;
}

尾插

void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead);//phead->prev就是尾ListNode* tail = phead->prev;ListNode* newnode = BuyListNode(x);tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}
void ListPushBack(ListNode* phead, LTDataType x)
{LIstInsert(phead, x);
}
//尾插相当于在phead前面插

image-20220210144718867

命名风格:驼峰法,PopBack
变量的第一个单词小写 toDelete

头插

void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);ListNode* first = phead->next;ListNode* newnode = BuyListNode(x);//phead newnode firstphead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode;
}
void ListPushFront(ListNode* phead, LTDataType x)
{LIstInsert(phead->next, x);
}
//头插相当于在phead->next的前面去插入

尾删

void ListPopBack(ListNode* phead)
{assert(phead);assert(phead != phead->next);//只有一个头节点没法删ListNode* tail = phead->prev;ListNode* tailPrev = tail->prev;free(tail);tail = NULL;tailPrev->next = phead;phead->prev = tailPrev;
}
void ListPopBack(ListNode* phead)
{ListErase(phead->prev);
}

头删

void ListPopFront(ListNode* phead)
{assert(phead);assert(phead != phead->next);//只有一个头节点没法删ListNode* toDelete = phead->next;ListNode* first = toDelete->next;free(toDelete);toDelete = NULL;//删到只剩头节点时,满足自己指向自己phead->next = first;first->prev = phead;
}
void ListPopFront(ListNode* phead)
{ListErase(phead->next);
}

查找

ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

插入

void LIstInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* prev = pos->prev;ListNode* newnode = BuyListNode(x);//在pos前面插入//prev newnode posprev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;}

擦除

void ListErase(ListNode* pos)
{assert(pos);ListNode* prev = pos->prev;ListNode* next = pos->next;//prev pos nextprev->next = next;next->prev = prev;free(pos);
}

是否为空

int ListEmpty(ListNode* phead)//空返回1,非空返回0
{assert(phead);return phead->next == phead ? 1 : 0;
}

计算大小

int ListSize(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;int size = 0;while (cur != phead){++size;cur = cur->next;}return size;
}

摧毁

void ListDestroy(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = NULL;cur = next;}free(phead);phead = NULL;//这样不起作用的,除非传二级指针或者调用之后再手动置空//不用二级保持接口一致性
}
//s
ListDestroy(plist);
plist = NULL;

练习

OJ调试技巧

  1. 看描述是什么类型的错误,其次看测试用例,针对测试用例走读代码
  2. 如果还分析不出来,借助单个用例,进行打印相关值观察
  3. 拷贝到IDE上调试

27. 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

//暴力双循环,只不过时间复杂度不符合
int removeElement(int* nums, int numsSize, int val){int size = numsSize;for(int i = 0; i<numsSize;i++){if(nums[i] == val){for(int j=i+1;j<size;j++){nums[j-1] = nums[j];}i--;size--;}}return size;
}
//快慢指针
int removeElement(int* nums, int numsSize, int val){int left = 0;for(int right = 0; right<numsSize;right++){if(nums[right] != val){nums[left] = nums[right];left++;}}return left;
}

image-20220205172214335

int removeElement(int* nums, int numsSize, int val)
{int src = 0;int dst = 0;while(src < numsSize){if(nums[src] == val){++src;}else{nums[dst] = nums[src];++src;++dst;}}return dst;
}

26. 删除有序数组中的重复项

//k
int removeDuplicates(int* nums, int numsSize){if(numsSize == 0){return 0;}int fast = 1;int slow = 1;while(fast<numsSize){if(nums[fast] != nums[fast-1]){nums[slow++] = nums[fast++];}else{++fast;}}return slow;}

989. 数组形式的整数加法

/*** Note: The returned array must be malloced, assume caller calls free().*/
int* addToArrayForm(int* num, int numSize, int k, int* returnSize){//不确定加完的数有多大//考虑到有进位,要多开1位,最多进1位int kSize = 0;int nums = k;while(nums){++kSize;nums /= 10;}int len = kSize > numSize ? kSize+1:numSize+1;int* retArr = (int*)malloc(sizeof(int)*len);//倒着取数组的值并于k的每一位逐个相加int Ai = numSize-1;int ki = 0;int next = 0;//进位int reti = 0;while(Ai >= 0 || ki <kSize){int aVal = 0;if(Ai >= 0){//考虑到数组比K短aVal = num[Ai--];}int kVal = k%10;k /= 10;ki++; int ret = aVal + kVal +next;if(ret >= 10){next = 1;ret -= 10;}else{next = 0;}//正着放不确定从最后一位开始放还是倒数第二位开始放//倒着放到数组,再把数组倒置回来retArr[reti++] = ret;}if(next == 1){retArr[reti++] = 1;//还有一个进位没放要放进去}//逆置int begin = 0;int end = reti - 1;while(begin<end){int tmp = retArr[begin];retArr[begin] = retArr[end];retArr[end] = tmp;begin++;end--;}*returnSize = reti;return retArr;
}

88. 合并两个有序数组

思路一:

image-20220206184107071

题目给的nums1空间大小是m+n,后面的n个位置是空的,就是为了放nums2的元素

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){int p1 = 0;int p2 = 0;int sorted[m+n];while(p1<m || p2<n){if(p1 == m){sorted[p1+p2-1] = nums2[p2++];}else if(p2 == n){sorted[p1+p2-1] = nums1[p1++];}else if(nums1[p1] < nums2[p2]){sorted[p1+p2-1] = nums1[p1++];}else{sorted[p1+p2-1] = nums2[p2++];}}for(int i = 0;i < m+n;i++){nums1[i] = sorted[i];}
}
//额外开辟数组,空间复杂度是O(N)

思路二:大的从后往前放,不需额外开辟数组

image-20220206184240612

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(nums1[end1]>nums2[end2]){nums1[end--] = nums1[end1--];}else{nums1[end--] = nums2[end2--];}}//end2结束了,end1还有数据,无需挪动//end1先结束,需要把end2的数据放到nums1里面while(end2>=0){nums1[end--] = nums2[end2--];}
}
//时间复杂度O(m+n) 空间复杂度O(1)

另一种写法

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(-1 == end1){//end1为-1说明nums1里面没有元素nums1[end--] = nums2[end2--];}else if(-1 == end2){//nums2没有元素nums1[end--] = nums1[end1--];}else if(nums1[end1]>=nums2[end2]){//谁大谁就放进nums1[end--] = nums1[end1--];}else if(nums1[end1] <= nums2[end2]){nums1[end--] = nums2[end2--];}}
}

189. 轮转数组

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

思路一:两层循环

void rotate(int* nums, int numsSize, int k){int end = numsSize-1;int temp = 0;for(int i=0; i<k; i++){temp = nums[end];for(int j=end; j>0; j--){nums[j] = nums[j-1];}nums[0] = temp;}
}
//时间复杂度O(N*K) 空间复杂度O(1)

思路二:以空间换时间

void rotate(int* nums, int numsSize, int k){int newArr[numsSize];for(int i=0; i<numsSize; i++){newArr[(i+k)%numsSize] = nums[i];}for(int i=0; i<numsSize; i++){nums[i] = newArr[i];}
}
//时间复杂度O(N) 空间复杂度O(N)

思路三 3次逆置

//对原数组后k个数逆置
//对原数组前n-k个数逆置
//对原数组整体逆置
void Reverse(int* arr, int left, int right){while(left<right){int tmp = arr[right];arr[right] = arr[left];arr[left] = tmp;left++;right--;}
}void rotate(int* nums, int numsSize, int k){if(k>numsSize){k %= numsSize;}Reverse(nums, 0, numsSize-k-1);Reverse(nums, numsSize-k, numsSize-1);Reverse(nums, 0, numsSize-1);
}
//空间复杂度O(1),时间复杂度O(N)

203. 移除链表元素

考虑清楚多种情况再写代码,

image-20220208154544307

如果第一个值就是val需要单独考虑,prev为NULL的情况

struct ListNode* removeElements(struct ListNode* head, int val){struct ListNode* cur = head;struct ListNode* prev = NULL;while(cur != NULL){struct ListNode* next = cur->next;if(cur->val == val){if(prev == NULL){//第一个值就是val的情况free(cur);head = next;cur = next;}   else{prev->next = next;free(cur);cur = next;}}else{prev = cur;cur = cur->next;}}return head;//返回头指针,就不需要传二级指针修改头指针
}

带哨兵位的

image-20220208160525635

struct ListNode* removeElements(struct ListNode* head, int val){struct ListNode* guardHead = (struct ListNode*)malloc(sizeof(struct ListNode));guardHead->next = head;struct ListNode* cur = head;struct ListNode* prev = guardHead;while(cur != NULL){if(cur->val == val){struct ListNode* next = cur->next;prev->next = next;free(cur);cur = next;}else{prev = cur;cur = cur->next;}}head = guardHead->next;free(guardHead);guardHead = NULL;return head;
}

206. 反转链表

思路一: 三指针翻转

image-20220208162931119

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
struct ListNode* reverseList(struct ListNode* head){if(head == NULL || head->next == NULL){//空链表和只有一个节点的情况return head;}struct ListNode* n1 = NULL;struct ListNode* n2 = head;struct ListNode* n3 = head->next;while(n2 != NULL){//翻转n2->next = n1;//迭代n1 = n2;n2 = n3;if(n3 != NULL){//n2指向最后一个元素时,n3指向的是NULL,不能解引用NULLn3 = n3->next;}}head = n1;return head;
}

思路二:头插法

image-20220208164729468

struct ListNode* reverseList(struct ListNode* head){//包含了空链表和只有一个节点的情况//头插法struct ListNode* cur = head;struct ListNode* newHead = NULL;while(cur != NULL){struct ListNode* next = cur->next;cur->next = newHead;newHead = cur;cur = next;}head = newHead;return head;
}

思路三:递归

在稿纸上画图分析就行,递归核心就是假设后面n-1个已经翻转了

//1->2<-3<-4<-5
//而且第一个要指向NULL
struct ListNode* reverseList(struct ListNode* head){//递归if(head == NULL || head->next == NULL){return head;}struct ListNode* cur = head;struct ListNode* next = cur->next;struct ListNode* newHead = reverseList(next);next->next = cur;cur->next = NULL;return newHead;
}

876. 链表的中间结点

image-20220208170245009

struct ListNode* middleNode(struct ListNode* head){struct ListNode* slow = head;struct ListNode* fast = head;//快慢指针,快指针一次走2步while(fast && fast->next){slow = slow->next;fast = fast->next->next;}return slow;
}

剑指 Offer 22. 链表中倒数第k个节点

思路一:暴力循环

struct ListNode* getKthFromEnd(struct ListNode* head, int k){//求链表长度int len = 0;struct ListNode* cur = head;while(cur != NULL){cur = cur->next;len++;}//倒数第k个就是正数第len-k次访问for(int i=0; i<len-k; i++){head = head->next;}return head;
}

思路二:快慢指针

struct ListNode* getKthFromEnd(struct ListNode* head, int k){//快慢指针struct ListNode* slow = head;struct ListNode* fast = head;//fast先走k步while(k--){//考虑空链表以及k>链表长度的问题if(fast == NULL){return NULL;}fast = fast->next;}//快慢同时走while(fast != NULL){fast = fast->next;slow = slow->next;}return slow;
}

牛客的OJ很恶心,很难调
1.自测常规情况,可以过
2.打印一些标记值,不显示输出标记值.比如:段错误
3.思考极端情况场景测试用例(链表为空,k大于链表长度,k为负数等等)

力扣环境很舒服,错了会报测试用例,根据测试用例去改就行

21. 合并两个有序链表

image-20220208184011830

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){struct ListNode* head = NULL;struct ListNode* tail = NULL;if(list1 == NULL && list2 != NULL){return list2;}if(list2 == NULL && list1 != NULL){return list1;}if(list1 == NULL && list2 == NULL){return NULL;}//先取个小的去做头,方便后面尾插if(list1->val < list2->val){head = tail = list1;list1 = list1->next;}else{head = tail = list2;list2 = list2->next;}while(list1 && list2){if(list1->val < list2->val){tail->next = list1;list1 = list1->next;tail = tail->next;}else{tail->next = list2;list2 = list2->next;tail = tail->next;}}//l1或者l2为空就出来了if(list1 == NULL){tail->next = list2;}else if(list2 == NULL){tail->next = list1;}return head;
}
if(list1 == NULL && list2 != NULL){return list2;}if(list2 == NULL && list1 != NULL){return list1;}if(list1 == NULL && list2 == NULL){return NULL;}
//可以写得简洁一点
if(list1 == NULL){return list2;}if(list2 == NULL){return list1;}

思路二:哨兵位

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){struct ListNode* head = NULL;struct ListNode* tail = NULL;//带哨兵位的写法head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));tail->next = NULL;while(list1 && list2){if(list1->val < list2->val){tail->next = list1;list1 = list1->next;tail = tail->next;}else{tail->next = list2;list2 = list2->next;tail = tail->next;}}//l1或者l2为空就出来了if(list1){tail->next = list1;}else if(list2){tail->next = list2;}//开辟的哨兵位用完要释放struct ListNode* node = head;head = head->next;free(node);node = NULL;return head;
}

链表分割

现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:ListNode* partition(ListNode* pHead, int x) {//借助哨兵位struct ListNode* lessHead = NULL;struct ListNode* lessTail = NULL;struct ListNode* greaterHead = NULL;struct ListNode* greaterTail = NULL;//开辟哨兵位lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));lessTail->next = NULL;greaterTail->next = NULL;//把小于x的插到一个链表,大于x的插到一个链表while(pHead != NULL){if(pHead->val < x){lessTail->next = pHead;pHead = pHead->next;lessTail = lessTail->next;}else{greaterTail->next = pHead;pHead = pHead->next;greaterTail = greaterTail->next;}}//链接2个链表lessTail->next = greaterHead->next;//防止成环,要让最后的那个值指向NULLgreaterTail->next = NULL;pHead = lessHead->next;//释放空间free(lessHead);lessHead = NULL;free(greaterHead);greaterHead = NULL;return pHead;}
};

image-20220208195835889

牛客的调试太难了,只能靠猜测极端情况

1.空链表过得去
2.全大于/全小于x也过得去
3.有可能成环

成环了就陷入死循环,牛客就会报内存受限,其实不是真的内存不足,而是要考虑死循环的情况

VS下调试链表的题目

//在VS下调试链表的题目
struct ListNode {int val;struct ListNode *next;ListNode(int x) : val(x), next(NULL) {}
};class Partition {
public:ListNode* partition(ListNode* pHead, int x) {//借助哨兵位struct ListNode* lessHead = NULL;struct ListNode* lessTail = NULL;struct ListNode* greaterHead = NULL;struct ListNode* greaterTail = NULL;//开辟哨兵位lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));if (lessTail == NULL){return 0;}if (lessHead == NULL){return 0;}if (greaterHead == NULL){return 0;}if (greaterTail == NULL){return 0;}lessTail->next = NULL;greaterTail->next = NULL;//把小于x的插到一个链表,大于x的插到一个链表while (pHead != NULL) {if (pHead->val < x) {lessTail->next = pHead;pHead = pHead->next;lessTail = lessTail->next;}else {greaterTail->next = pHead;pHead = pHead->next;greaterTail = greaterTail->next;}}//链接2个链表lessTail->next = greaterHead->next;//防止成环,要让最后的那个值指向NULLgreaterTail->next = NULL;pHead = lessHead->next;//释放空间free(lessHead);lessHead = NULL;free(greaterHead);greaterHead = NULL;return pHead;}
};void PrintList(struct ListNode* cur) 
{while (cur){printf("%d->", cur->val);cur = cur->next;}printf("NULL\n");
}int main()
{struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n7 = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* n8 = (struct ListNode*)malloc(sizeof(struct ListNode));if (n1 && n2 && n3 && n4 && n5 && n6 && n7 && n8)//防止开辟失败返回NULL程序崩溃{n1->val = 2;n2->val = 4;n3->val = 9;n4->val = 3;n5->val = 2;n6->val = 1;n7->val = 6;n8->val = 3;n1->next = n2;n2->next = n3;n3->next = n4;n4->next = n5;n5->next = n6;n6->next = n7;n7->next = n8;n8->next = NULL;}struct ListNode* head = Partition().partition(n1, 5);PrintList(head);return 0;
}

image-20220208201555230

链表的回文结构

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。 给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

思路一:开一个int[900]的数组,用数组判断,虽然也能通过牛客的判断,但实际不符合空间复杂度O(1)的要求

思路二: 1.先找到中间节点 2.翻转后半部分链表 3.一一比较

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public://先找到中间节点struct ListNode* midNode(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* newHead = NULL;struct ListNode* cur = head;while(cur != NULL){struct ListNode* next = cur->next;cur->next = newHead;newHead = cur;cur = next;}head = newHead;return head;//再一一比较}bool chkPalindrome(ListNode* A) {struct ListNode* mid = midNode(A);struct ListNode* rHead = ReverseList(A);while(A && rHead){if(A->val == rHead->val){A = A->next;rHead = rHead->next;}else{return false;}}return true;}
};

image-20220208204007448

奇数个是怎么过的呢?奇数个理论上不是应该让中间的前一个指向NULL才行吗?

其实是虽然后半部分逆置了,但2还是指向3的,3的next为NULL

image-20220208204625803

160. 相交链表

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{//考虑空链表if(headA == NULL || headB == NULL){return NULL;}//先算2个链表长度int lenA = 0;int lenB = 0;struct ListNode* curA = headA;struct ListNode* curB = headB;while(curA->next != NULL){curA = curA->next;lenA++;}//各自都少计算一步,相减没影响,这样方便后面判断是否相交while(curB->next != NULL){curB = curB->next;lenB++;}//不相交,此时curA->nxet == NULLif(curA != curB){return NULL;}//注意要把curA curB的位置调整回到一开始curA = headA;curB = headB;//让长的链表先把多出来的长度走了,再一起走if(lenA > lenB){int len = lenA - lenB;while(len--){curA = curA->next;}}if(lenA < lenB){int len = lenB - lenA;while(len--){curB = curB->next;}}//一起走,看谁的指针相同while(curA != curB){curA = curA->next;curB = curB->next;}return curA;
}

另一种写法,长度计算用abs

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{//考虑空链表if(headA == NULL || headB == NULL){return NULL;}//先算2个链表长度int lenA = 0;int lenB = 0;struct ListNode* curA = headA;struct ListNode* curB = headB;while(curA->next != NULL){curA = curA->next;lenA++;}//各自都少计算一步,相减没影响,这样方便后面判断是否相交while(curB->next != NULL){curB = curB->next;lenB++;}//不相交,此时curA->nxet == NULLif(curA != curB){return NULL;}//另一种写法struct ListNode* longList = headA;struct ListNode* shortList = headB;if(lenB > lenA){longList = headB;shortList = headA;}int gap = abs(lenB - lenA);while(gap--){longList = longList->next;}while(longList != shortList){longList = longList->next;shortList = shortList->next;}return longList;
}

不先判断是否相交也行,如果不相交最后返回的也是NULL

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{//考虑空链表if(headA == NULL || headB == NULL){return NULL;}//先算2个链表长度int lenA = 0;int lenB = 0;struct ListNode* curA = headA;struct ListNode* curB = headB;while(curA != NULL){curA = curA->next;lenA++;}//各自都少计算一步,相减没影响,这样方便后面判断是否相交while(curB != NULL){curB = curB->next;lenB++;}//另一种写法struct ListNode* longList = headA;struct ListNode* shortList = headB;if(lenB > lenA){longList = headB;shortList = headA;}int gap = abs(lenB - lenA);while(gap--){longList = longList->next;}while(longList != shortList){longList = longList->next;shortList = shortList->next;}return longList;
}

141. 环形链表

判断是否为环形链表

//快慢指针
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(slow == fast){return true;}}return false;
}

延申问题:

  1. 为什么slow走一步,fast走两步,他们一定会在环内相遇呢,会不会永远追不上?请证明
    不会.假设slow进环的时候,fast跟slow的距离是N,紧接着追击的过程中,slow每走一步,fast走2步,也就是他们自己距离缩减1,迟早会追上
  2. slow走一步,fast走3步,4步,x步呢?一定会相遇吗
    不一定.走3步时,他们每走一次,距离缩小2步,如果N是偶数就会相遇,奇数就不会相遇,变成-1时,实际相差C-1,如果C-1也是奇数,那么永远追不上
  3. 求环的入口点
    由图推出公式:2(L+X) = L+n * C+X
    L是起点到入口点的距离,X是相遇点到入口点的距离,环的长度是C
    L = n * C-x
    也就是一个指针从head走,一个从meet走,他们会在入口相遇

image-20220208225536582

142. 环形链表 II

image-20220209000516972

struct ListNode *detectCycle(struct ListNode *head) {struct ListNode* slow = head;struct ListNode* fast = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast){//相遇//通过证明,一个指针从head走//一个从meet走,他们会在入口相遇struct ListNode* meet = fast;while(meet != head){meet = meet->next;head = head->next;}return head;}}return NULL;
}

思路二:转换成相交问题

image-20220209115208730

//把环折断,转化为相交问题 注意要翻转链表,且找到入口之后还有翻转回来
struct ListNode* reverseList(struct ListNode* head){//包含了空链表和只有一个节点的情况//头插法struct ListNode* cur = head;struct ListNode* newHead = NULL;while(cur != NULL){struct ListNode* next = cur->next;cur->next = newHead;newHead = cur;cur = next;}head = newHead;return head;
}struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{//考虑空链表if(headA == NULL || headB == NULL){return NULL;}//先算2个链表长度int lenA = 0;int lenB = 0;struct ListNode* curA = headA;struct ListNode* curB = headB;while(curA->next != NULL){curA = curA->next;lenA++;}//各自都少计算一步,相减没影响,这样方便后面判断是否相交while(curB->next != NULL){curB = curB->next;lenB++;}//不相交,此时curA->nxet == NULLif(curA != curB){return NULL;}//另一种写法struct ListNode* longList = headA;struct ListNode* shortList = headB;if(lenB > lenA){longList = headB;shortList = headA;}int gap = abs(lenB - lenA);while(gap--){longList = longList->next;}while(longList != shortList){longList = longList->next;shortList = shortList->next;}return longList;
}struct ListNode *detectCycle(struct ListNode *head) {struct ListNode* slow = head;struct ListNode* fast = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast){//相遇//转换成相交问题struct ListNode* meet = fast;struct ListNode* newHead = meet->next;//相交点断开环meet->next = NULL;//需要把head到meet的链表反转struct ListNode* rhead = reverseList(head);struct ListNode* node = getIntersectionNode(newHead,rhead);//再把环恢复原样head = reverseList(rhead);meet->next = newHead;return node;}}return NULL;
}

138. 复制带随机指针的链表

struct Node* copyRandomList(struct Node* head) {//考虑空链表if(head == NULL){return NULL;}//1.拷贝节点挂到原节点的后面,建立对应关系struct Node* cur = head;while(cur != NULL){struct Node* next = cur->next;struct Node* copy = (struct Node*)malloc(sizeof(struct Node));copy->val = cur->val;cur->next = copy;copy->next = next;cur = next;}//2.处理copy节点的randomcur = head;while(cur != NULL){struct Node* copy = cur->next;if(cur->random == NULL){copy->random = NULL;}else{copy->random = cur->random->next;}cur = copy->next;}//3.拷贝节点取下来链接到一起,恢复原链表cur = head;//借助哨兵位struct Node* copyHead = NULL;struct Node* copyTail = NULL;copyHead = copyTail = (struct Node*)malloc(sizeof(struct Node));while(cur != NULL){struct Node* copy = cur->next;struct Node* next = copy->next;//尾插copyTail->next = copy;copyTail = copyTail->next;cur->next = next;cur = next;}struct Node* guard = copyHead;copyHead = copyHead->next;free(guard);guard = NULL;return copyHead;
}

image-20220209135711680

image-20220209135955021

image-20220209140716391

147. 对链表进行插入排序

image-20220210132246016

struct ListNode* insertionSortList(struct ListNode* head){//空链表或单个节点if(head == NULL || head->next == NULL){return head;}//1.初始条件struct ListNode* sortHead = head;struct ListNode* cur = head->next;sortHead->next = NULL;while(cur)//2.终止条件{//3.迭代条件struct ListNode* next = cur->next;//将cur节点插入到前面有序区间struct ListNode* p = NULL;struct ListNode* c = sortHead;while(c){if(cur->val < c->val){break;}else{p = c;c = c->next;}}if(p == NULL)//头插{cur->next = c;sortHead = cur;}else//插中间和尾插{p->next = cur;cur->next = c;}cur = next;}return sortHead;
}

删除链表中重复的结点

自己考虑好极端情况,空链表,单节点,元素都相同

image-20220210140650816

struct ListNode* deleteDuplication(struct ListNode* pHead ) {//空链表或一个节点if(pHead == NULL || pHead->next == NULL){return pHead;}//1.起始条件struct ListNode* prev = NULL;struct ListNode* cur = pHead;struct ListNode* next = pHead->next;while(next){if(cur->val == next->val){//删除重复的//让next往前走,直到碰到不相等的//如果最后几个元素都相等,那么next有可能走到空while(next && next->val == cur->val){next = next->next;}if(prev == NULL){pHead = next;cur = next;if(next != NULL){next = next->next;}}if(prev != NULL){cur = next;prev->next = next;if(next != NULL){next = next->next;}}}else{prev = cur;cur = next;if(next != NULL){next = next->next;}}}return pHead;
}

另一种写法:三指针

struct ListNode* deleteDuplication(struct ListNode* pHead ) {//空链表或一个节点if(pHead == NULL || pHead->next == NULL){return pHead;}//1.起始条件struct ListNode* prev = NULL;struct ListNode* cur = pHead;struct ListNode* next = pHead->next;while(next){if(cur->val == next->val){//删除重复的//让next往前走,直到碰到不相等的//如果最后几个元素都相等,那么next有可能走到空while(next && next->val == cur->val){next = next->next;}//删除cur到next之间的节点while(cur != next){struct ListNode* del = cur;cur = cur->next;free(del);del = NULL;}//考虑是否需要改变pHeadif(prev == NULL){pHead = cur;}else{prev->next = cur;}if(next != NULL){next = next->next;}}else{prev = cur;cur = next;next = next->next;}}return pHead;
}
http://www.jmfq.cn/news/5074939.html

相关文章:

  • wordpress 抓取时间长/ios aso优化工具
  • 西安做网站公司必达/百度关键词多少钱一个月
  • 多国语言交友网站建设/推广产品的方法
  • wordpress基础主题站/石家庄最新消息今天
  • 公司网站制作教学/磁力王
  • 成都黑帽seo/新乡seo顾问
  • ecshop 获取网站域名/附近学电脑培训班
  • 北京正规网站建设有几种/百度推广优化是什么?
  • 深圳建设工程交易服务网站/短视频seo排名系统
  • 北京电商网站建设哪家好/seo草根博客
  • 广东微信网站开发哪家好/微信软文是什么
  • 网站建设-设计/独立站seo外链平台
  • 虎门网站建设服务商/优秀的软文
  • 网站的虚拟人怎么做的/市场推广计划怎么写
  • 2021网站你懂我意思正能量/电商网站网址
  • 哪些网站是做婴童辅食招商的/企业网站排名优化
  • ps做的网站保存不了jpg/如何做好搜索引擎优化工作
  • 作品设计思路范文/百家号关键词排名优化
  • 做封面的网站在哪里/软文怎么写吸引人
  • wordpress 换行用/seo排名点击器曝光行者seo
  • 江苏中小企业建站价格/全网营销与seo
  • 武汉网络兼职网站建设/杭州网站推广找哪家
  • wordpress如何启用全站ssl/宁波网站推广大全
  • 简单logo设计/短视频seo关键词
  • webstorm做网站/搜索引擎优化概述
  • 竞价网站服务器/打开百度一下网页版
  • 哪里有网站建设加盟合作/常州百度推广代理
  • 如何做公司网站的/关键词seo公司
  • 做单页网站需要做什么/济南疫情最新消息
  • 张家界企业网站制作/推广什么软件可以长期赚钱