目录
5、选择排序时间 复杂度:最坏情况:O(N^2)最好情况:O(N^2)空间复杂度:O(1)
链表
1、实现链表的逆置
逆置链表有两种方式,
第一种是新建链表,将原链表的每个节点都头插到新链表中;
第二种就是在原链表上操作,利用三个遍历指针来改变原链表中每个节点的next指向;
遍历链表时要注意链表的结束条件
ListNode* ReverseList(ListNode* pHead) {
if (!pHead) return nullptr;
vector<ListNode*> v;
while (pHead) {
v.push_back(pHead);
pHead = pHead->next;
}
reverse(v.begin(), v.end()); // 反转vector,也可以逆向遍历
ListNode *head = v[0];
ListNode *cur = head;
for (int i=1; i<v.size(); ++i) { // 构造链表
cur->next = v[i]; // 当前节点的下一个指针指向下一个节点
cur = cur->next; // 当前节点后移
}
cur->next = nullptr; // 切记最后一个节点的下一个指针指向nullptr
return head;
}
struct ListNode {
int val;
struct ListNode *next;
}
struct ListNode* ReverseList(struct ListNode* pHead ) {
struct ListNode *pre=NULL;
struct ListNode *cur=pHead;
struct ListNode *nex=NULL;
while(cur)
{
nex=cur->next;
cur->next=pre;
pre=cur;
cur=nex;
}
return pre;
}
2、判断单链表中是否有环
在有环的情况下,慢指针一次走一个步长,快指针一次走两个步长,第一次相聚的地方为相聚点(证明有环)
bool hasCycle(struct ListNode* head ) {
if(head==NULL)
return false;
struct ListNode *slow,*fast;
slow=head;
fast=head;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
return true;
}
return false;
}
3、单链表相交,如何求交点
思路:获取两个链表长度;将较长链表移动和锻炼表长度差值位置;移动后两个链表长度一样,然后一起移动,节点相等时链表相交;
//L1和L2为两个单链表,函数返回True表示链表相交,返回false表示不相交
typedef struct Link{
int elem;
struct Link* next;
}link;
bool ListIntersect(link* L1 , link* L2){
Link* plong = L1;
Link* pshort = L2;
Link* temp = NULL;
int num1 = 0,num2 = 0,step = 0;
//得到L1的长度
while(plong){
num1++;
plong = plong->next;
}
while(pshort){
num2++;
pshort = pshort->next;
}
//重置plong和pshort
plong = L1;
pshort = L2;
step = num1-num2;
if(num1<num2){
plong=L2;
pshort = L1;
step = num2-num1;
}
temp = plong;
while(step){
temp = temp->next;
step--;
}
while(temp&&pshort){
if(temp==pshort){
return true;
}
temp = temp->next;
pshort = pshort->next;
}
return false;}
方法2:使用两个指针,一个从链表1的头节点开始遍历,记为P;一个从链表2的头节点开始遍历,记为q;让p和q一起遍历,当p走完链表1时(p=NULL ),则从链表2的头节点继续访问,同样的,如果q先走完了链表2,那么继续从链表1的头节点进行遍历。
因为两个指针,同样的速度,走完同样的长度(链表1+链表2),不管两个链表是否有相同节点,都能够同时达到终点(p最后肯定能达到链表2的终点,q最后肯定能达到链表1的终点)
所以如何得到公共节点呢?
1、在有公共节点的时候,q和p必定会相遇,因为长度和速度都是一样的,必定也会走到相同的地方,所以当两者相等的时候,就是第一个公共节点
2、在没有公共节点的时候,p和q都会走到终点,此时都是NULL,也算是相等了;
struct ListNode* FindFirstCommonNode(struct ListNode* pHead1;struct ListNode* pHead2){
struct ListNode* p = pHead1;
struct ListNode* q = pHead2;
while(p!=q){
p = (p == NULL ? pHead2 :p->next);
q = (q == NULL ? pHead1 :q->next);
}
return p;
}
4、求有环链表第一个入环节点
1、首先判断是否有环,有环时,有环时返回相遇节点,无环返回NULL;
2、在有环的情况下,求链表的入环节点,慢指针一次走一个步长,快指针一次走两个步长,第一次相聚的地方就是相聚点
fast 从头出发,一次一步;
slow从相遇点出发,一次一步;再次相遇就是环入口点;
证明:
快指针与慢指针均从X出发,在Z相遇。此时,慢指针行使距离为a+b,快指针为a+b+n(b+c)。所以2*(a+b)=a+b+n*(b+c),推出
a=(n-1)*b+n*c=(n-1)(b+c)+c;
得到,将此时两指针分别放在起始位置和相遇位置,并以相同速度前进,当一个指针走完距离a时,另一个指针恰好走出 绕环n-1圈加上c的距离。
struct ListNode* getIntersectionNode(struct ListNode* head){
if(head==NULL){
return NULL;
}
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast!=NULL&&fast->next!=NULL){
fast = fast->next->next;
slow = slow->next;
if(fast==slow){
break;
}
}
if(fast == NULL){
return NULL;
}
slow = head;
while(fast!=slow){
slow = slow->next;
fast = fast->next;
}
return slow;
}
5、删除链表的第i个节点
struct ListNode* removeNode(struct ListNode* pHead, int n){
struct ListNode* temp = pHead;
int i = 0;
for(i=0;i<n;i++){
temp = temp->next;
}
temp->next=temp->next->next;
free(temp->next);
return pHead;
}
6、删除链表倒数第n个节点
struct ListNode* removeNthFromEnd(struct ListNode* head,int n){
if(head==NULL)
return NULL;
struct ListNode* temp=malloc(sizeof(struct ListNode));
temp->next=head;
struct ListNode* fast = temp;
struct ListNode* slow = temp;
struct ListNode* cur = slow;
for(int i=0;i<n;i++){
fast = fast->next;
while(fast!=NULL){
fast = fast->next;
cur = slow;
slow = slow->next;
}
cur->next = cur->next->next;
return temp;
}
7、迭代算法实现两个有序链表的合并
思路:
如果pHead1的节点值小于pHead2的节点值,则将pHead1的指针节点值链接到cur的next指针,然后pHead1指向下一个节点值
否则让pHead2指向下一个节点值
循环步骤,直到pHead1或者pHead2为空,将phead1或者phead2的剩下部分链接到cur后面
struct ListNode* meger(struct ListNode* pHead1,struct ListNode* pHead2){
struct ListNode* cur = malloc(sizeof(struct ListNode));
struct ListNode* temp = cur;
while(pHead1&&pHead2){
if(pHead1->val<=pHead2->val){
cur->next = pHead1;
pHead1=pHead1->next;
}
else{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
cur->next = pHead1 ?pHead1 : pHead2;
return temp->next;
}
8、递归实现两个有序链表的合并
思路:特殊情况,有一个链表为空,则返回另一个链表
如果pHead1的节点值比pHead2小,下一个节点应该时pHead1,也就是return pHead1,在return之前,指定pHead1的下一个节点应该是pHead1->next和pHead2两链表合并后的头节点
如果pHead2的节点值比pHead1小,下一个节点应该时pHead2,也就是return pHead2,在return之前,指定pHead2的下一个节点应该是pHead2->next和pHead1两链表合并后的头节点
struct ListNode* meger(struct ListNode* pHead1,struct ListNode* pHead2){
if(pHead1==NULL)
return pHead2;
if(pHead2==NULL)
return pHead1;
struct ListNode* temp = malloc(sizeof(struct ListNode));
temp->next = NULL;
if(pHead1->val<=pHead2->val){
temp = pHead1;
temp->next = meger(pHead1->next,pHead2);
}
else{
temp = pHead2;
temp->next = meger(pHead2->next,pHead1);
}
return temp;
}
9、求链表倒数第k个节点
思路1:快慢指针求解,fast比slow先多走K步,然后再走完之后两个指针一起走,直到fast为空,slow就是倒数第k个节点
struct ListNode* FindKthTotail(struct ListNode*pHead,int k){
struct ListNode* fast = pHead;
struct ListNode* slow = pHead;
while(k--){
if(!fast){
return NULL;
}
fast = fast->next;
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
10、删除链表倒数第K个节点
struct ListNode* removeNthFromEnd(struct ListNode* pHead ,int n)
{
if(pHead==NULL)
return NULL;
struct ListNode* temp =malloc(sizeof(struct ListNode));
temp->next=pHead;
struct ListNode* fast = pHead;
struct ListNode* slow = pHead;
while(n--){
fast = fast->next;
}
struct ListNode* cur = slow;
while(fast!=NULL){
fast = fast->next;
cur = slow;
slow = slow->next;
}
cur->next = cur->next->next;
return temp->next;
}
二叉树
二叉树主要有前序遍历,中序遍历以及后序遍历这三种主要的方式,父节点先于子节点就是先序遍历,子节点先于父节点就是后序遍历,对于子节点来说,如果先左节点然后父节点然后右节点就是中序遍历,那我们就先来解释下这种遍历的含义:
1、前序遍历:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
2、中序遍历:若二叉树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点
3、后序遍历:若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
先序遍历: A->B->D->E->C->F->G
中序遍历: D->B->E->A->F->C->G
后续遍历: D->E->B->F->G->C->A
先定义树节点
struct TreeNode {
TreeNode *left;
TreeNode *right;
int val;
};
1、前序(先序)遍历
定义:先访问父节点,然后遍历左子树,最后遍历右子树。
1、递归
1、采用递归的方法:A->B->D->E->C->F->G
void PreOrderTree(BiTree T) {
if (T==NULL) {
return;
}
// 遍历根节点(此处仅为输出,读者也可以根据实际需要进行处理,比如存储等)
//std::cout << root->val << std::endl;
printf("%c",T->data);
// 遍历左子树
PreOrderTree(T->lchild);
// 遍历右子树
PreOrderTree(T->rchild);
}
2、非递归
在采用非递归操作中,我们也是先访问根节点,然后遍历左子树,然后接着遍历右子树的方式
1、先到节点A,访问节点A,然后遍历A的左子树
2、到达节点B,访问节点B,然后遍历B的左子树
3、到达节点D,访问节点D,因为节点D无子树,因此节点D遍历完成
3.1节点D遍历完成,意味着节点B的左子树遍历完成,因此接着遍历节点B的右子树
4、到达节点E,访问节点E,因为节点E没有子树,所以节点E也完成遍历
4.1节点E遍历完成,意味节点B的右子树遍历完成,也代表着节点B的子树遍历完成
4.2开始遍历节点A的右子树
5、到达节点C,访问节点C,开始遍历C的左子树
6、到达节点F,访问节点F,因为节点F无子树,所以节点F遍历完成
6.1节点F遍历完成,意味着节点C的左子树遍历完成,因此下面开始遍历节点C的右子树
7、到达节点G,访问节点G,因为节点G无子树,因此节点G遍历完成
7.1节点G遍历完成,意味着节点C的右子树遍历完成,进而表示着节点C遍历完成
7.2节点C遍历完成,意味着节点A的右子树遍历完成,进而表示着节点A遍历完成,因此以A为根节点的树遍历完成
用非递归方式遍历二叉树,需要引入数据结构栈(stack),基本流程如下:
1、申请一个栈stack,,然后将头节点压入栈中;
2、从stack中弹出栈顶结点,打印;
3、将其右孩子节点(不为空的话)先压入栈中
4、然后将左孩子节点(不为空的话)压入栈中
5、不断重复234,直到stack为空,全部过程结束
void PreOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s;
s.push(root); // 步骤1
while (!s.empty()) {
auto t = s.top();
s.pop();//出栈
std::cout << t->val << std::endl; // 访问节点
if (t->right) {
s.push(t->right); // 对应步骤3
}
if (t->left) {
s.push(t->left); // 对应步骤4
}
}
}
2、中序遍历
先遍历左子树,访问根节点,遍历右子树
D->B->E->A->F->C->G
2.1 递归
void InOrder(TreeNode *root) {
if (!root) {
return;
}
// 遍历左子树
InOrder(root->left);
// 遍历根节点(此处仅为输出,读者也可以根据实际需要进行处理,比如存储等)
std::cout << root->val << std::endl;
// 遍历右子树
InOrder(root->right);
}
void InOrderTraverse(BiTree)
{
if(T==NULL)
return;
InOrderTraverse(T->lChild);
printf("%c",T->data);
InOrderTraverse(T->rChild);
}
上述中序遍历的递归代码,相比于先序遍历,只是将访问根节点的行为放在了遍历左右子树之间。
2.2 非递归
非递归操作中,我们仍然是按照先遍历左子树,然后访问根节点,最后遍历右子树的方式
中序遍历
1、到达节点A,节点A有左子树,遍历节点A的左子树
2、到达节点B,节点B有左子树,遍历节点B的左子树
3、到达节点D,节点D无子树,访问D节点
3.1由于D无子树,意味着B的左子树遍历完成,则返回B节点
4、访问B节点,遍历B节点的右子树
5、到达节点E,节点E无子树,访问节点E
5.1节点E遍历完成,意味着以B为根的子树遍历完成,回到A节点
6、到达节点A,访问节点A,遍历A节点的右子树
7、到达节点C,遍历C节点的左子树
8、到达节点F,因为F节点无子树,所以访问F节点
8.1由于F节点无子树,意味着C节点的左子树遍历完成,回到C节点
9、到达C节点,访问C节点,遍历C的右子树
10、到达G节点,由于G节点无子树,因此访问节点G
G节点遍历完成,意味着C节点的右子树遍历完成,也就是表达节点A的右子树遍历完成,意味着以A节点为根的二进制遍历完成
void InOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s;
auto p = root;
while (!s.empty() || p) {
if (p) { // 步骤1和2
s.push(p);
p = p->left;
} else { // 步骤4
auto t = s.top();
std::cout << t->val << std::endl;
p = t->right;
}
}
}
3、后序遍历(重点)
定义:先遍历左子树,再遍历右子树,最后访问根节点
D->E->B->F->G->C->A
3.1 递归![图六 后序遍历](https://i-blog.csdnimg.cn/blog_migrate/5356d46bebcb94eeeafecba13b6785aa.png)
void PostOrder(TreeNode *root) {
if (!root) {
return;
}
// 遍历左子树
PostOrder(root->left);
// 遍历右子树
PostOrder(root->right);
// 遍历根节点(此处仅为输出,读者也可以根据实际需要进行处理,比如存储等)
std::cout << root->val << std::endl;
}
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lChild);
PostOrderTraverse(T->rChild);
printf("%c",T->data);
}
前序遍历、中序遍历和后续遍历的递归遍历其实基本上都是一样的,唯一不同的就是访问根节点的代码位姿不一样
3.2 非递归
在非递归中,我们仍然是按照先遍历左子树,然后访问根节点,左后遍历右子树方式
后续遍历
1、到达节点A,遍历A的左子树
2、到达节点B,遍历节点B的左子树
3、到达节点D,由于D无子树,则访问节点D
3.1 节点D无子树,意味着节点B的左子树遍历完成,接着遍历B的右子树
4、到达节点E,由于E无子树,则访问节点E
4.1 节点E无子树,意味着节点B的右子树遍历完成,接回到节点B
5、访问节点B,回到节点B的根节点A
6、到达节点A,访问节点A的右子树
7、到达节点C,遍历节点C的左子树
8、到达节点F,由于节点F无子树,因此访问节点F
8.1 节点F访问完成,意味着C节点的左子树遍历完成,因此回到节点C
9、到达节点C,遍历节点C的右子树
10、到达节点G,由于节点G无子树,因为访问节点G
11、节点G访问完成,意味着C节点的右子树遍历完成,回到节点C
11.1 到达节点C,访问节点C
12、节点C遍历完成,意味着节点A的右子树遍历完成,回到节点A
12.1 节点A的右子树遍历完成,访问节点A
void PostOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s1;
std::stack<TreeNode*> s2;
s1.push(root);
while (!s1.empty()) {
auto t = s1.top();
s1.pop();
s2.push(t);
if (t->left) {
s1.push(t->left);
}
if (t->right) {
s1.push(t->right);
}
}
while (!s2.empty()) {
auto t = s2.top();
s2.pop();
std::cout << t->val << std::endl;
}
}
查找/排序算法及其改进
1、冒泡排序最坏时间复杂度是 O ( n 2 )
冒泡排序原始版
//冒泡排序两两比较的元素是没有被排序过的元素
public void bubbleSort(int[] array){
for(int i=0;i<array.length-1;i++){//控制比较轮次,一共 n-1 趟
for(int j=0;j<array.length-1-i;j++){//控制两个挨着的元素进行比较
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
此时冒泡排序的时间复杂度是O(N2),对冒泡排序算法可以进行优化
优化后的冒泡排序
优化点1、算法只需要执行到某趟遍历不需要交换元素位置即可,也就是说加上一个状态标志位,用于记录在此次遍历中是否发生了元素交换,如果没有发生变量交换,就停止循环;
优化点2、每趟遍历的区间右边界都是由上次发生的遍历的最后一次发生交换的位置决定的。
public void bubbleSort(int[] arr,int n)//这个n就是数组的长度
{
for(int i=0;i<n-1;i++)
{
boolean isSorted = true; //添加标记
for(int j=0;j<n-i-1;j++)
{
if(arr[j]>arr[j+1])
{
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
isSorted = false; //发生交换
}
}
if(isSorted==true)
{
break; //如果没有发生交换,就完成排序
}
}
}
2、快速排序
1. 首先取出一个key,一般取第一个元素
2. 从后往前遍历,如果数组中的数据小于了key,那么就将从前往后未比较过的第一个位置即fisrt位置替换为该数据
3. 然后从前往后遍历,如果数组中的数据大于了key,那么就将从后往前的第一个比较过数据位置替换
4. 直到左右两边的位置重合,说明key就找到了正确的位置,每次循环就能找到一个数的正确位置
5. 然后将key左右两边的数据分为两组,递归调用自己。
#include <stdio.h>
void quickSort(int arr[], int low, int high)
{
int first = low;
int last = high;
int key = arr[first];
if(low >= high)
return;
while(first < last)
{
while(first < last && arr[last] > key)
{
last--;
}
arr[first] = arr[last];
while(first < last && arr[first] < key)
{
first++;
}
arr[last] = arr[first];
}
arr[first] = key;
quickSort(arr, low, first-1);
quickSort(arr, first+1, high);
}
int main()
{
int i;
int a[10] = {3, 1, 11, 5, 8, 2, 0, 9, 13, 81};
for(i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
quickSort(a, 0, 9);
for(i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
3、堆排序
堆的性质:
堆中的某个节点的值总部不大于或者不小于其父节点的值
堆总是一个完全二叉树
堆排序:
https://blog.csdn.net/weixin_50886514/article/details/114847405
4、插入排序
5、选择排序时间 复杂度:最坏情况:O(N^2)最好情况:O(N^2)空间复杂度:O(1)
每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。
实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
//选择排序
void swap(int* a, int* b)
{
int tem = *a;
*a = *b;
*b = tem;
}
void SelectSort(int* arr, int n)
{
//保存参与单趟排序的第一个数和最后一个数的下标
int begin = 0, end = n - 1;
while (begin < end)
{
//保存最大值的下标
int maxi = begin;
//保存最小值的下标
int mini = begin;
//找出最大值和最小值的下标
for (int i = begin; i <= end; ++i)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//最小值放在序列开头
swap(&arr[mini], &arr[begin]);
//防止最大的数在begin位置被换走
if (begin == maxi)
{
maxi = mini;
}
//最大值放在序列结尾
swap(&arr[maxi], &arr[end]);
++begin;
--end;
}
}
6、二分查找->时间复杂度为O( log2N)
int search(int* nums,int numsLen,int target)
{
if(numsLen<=0)
return -1;
int slow = 0,fast = numsLen-1;
int mid = (slow+fast)/2;
while(slow<fast && nums[mid]!=target)
{
if(target>nums[mid])
{
slow = mid+1;
mid = (slow +fast)/2;
}
else
{
fast = mid-1;
mid = (slow + fast)/2;
}
}
return mid;
}
4、函数重写
4.1 strcat
char *my_strcat(char *dest,const char* src) //将源字符串加上const表明为输入参数,不可修改
{
char *strDest = dest; //保存字符串头字母地址
assert(dest!=NULL&&src!=NULL); //对源地址和目的地址加非0断言
while(*dest!='\0')
{
*dest++; //定位到字符串末尾
}
while((*dest++=*src++)!='\0')
return strDest;
}
4.2 验证大小端存储模式
大端存储模式:低字节存放在高地址中(低字节高地址);
小端存储模式:低字节存放在地址中(小端低低);
/*方法1*/
typedef union {
int i;
char c;
}my_union;
int checkSystem1(void)
{
my_union u;
u.i = 1;
return u.c;
}
/*方法2*/
int checkSystem2(void)
{
int i = 0x12345678;
char *c = &i;
return ((c[0] == 0x78) && (c[1] == 0x56) && (c[2] == 0x34) && (c[3] == 0x12));
}
4.3指针
int **a; //一个指向指针的指针,他指向的指针指向一个整数型
int *a[10]; //指针数组,该指针指向一个整型数;
int(*a)[10]; //数组指针,指向有10个整型数的数组
int(*a)(int); //函数指针,指向函数的指针,该函数有一个int的参数和一个int的返回值
int(*a[10])(int); //指针数组,该指针指向一个函数