首页 > 代码库 > 线性表

线性表

线性表是由 \(n\)个元素\((n \ge 0)\)组成的有限序列,线性表的特征

  • 所有数据元素类型相同
  • 线性表是由有限个元素构成
  • 线性表中的数据元素是与位置是有关的,这一点表明线性表不同于集合,线性表中每个元素都有一个对应的序号,线性表中的元素可以重复出现。

线性表的逻辑结构一般表示为:\( a_1,a_2,...,a_{i-1},a_i,a_{i+1},...,a_n \)

  • 除起始元素没有\(a_1\) 没有前驱元素之外,其他元素\(a_i\)有且仅有一个前驱元素\(a_{i-1},\)
  • 除终端元素没有后继元素外,其他元素 \(a_i\) 有且仅有一个后继元素\(a_{i+1}\)

线性表是一种逻辑结构,线性表的存储结构有

  • 顺序存储
  • 链式存储

顺序表

顺序表的特点

  • 属于直接映射(逻辑上相邻的元素,其物理位置也相邻),
  • 具有随机存取特性( 通过首地址和元素序号可以在 \(O(1)\) 时间内找到指定元素 )
  • 删除和插入元素需要移动大量元素。
    • 插入元素移动的平均次数\(=\frac{1}{n}\sum_{i=1}^{n}(n-i+1)=\frac{n}{2}\), 因此时间复杂度为\(O(n)\)
    • 删除元素移动的平均次数\(=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2}\),因此时间复杂度为 \(O(n)\)
  • 存储密度高,其值为\(1\)。\(存储密度=节点数据本身所占用的存储量/节点结构占用的存储量\)

描述顺序结构的三个属性

  • 顺序表的起始地址
  • 顺序表的最大长度
  • 顺序表的当前长度
# define MaxSize 100
typedef struct
   {
       int data[MaxSize];
       int length;
   } SqList;

给定一个有 \(n\) 个元素的整型数组\(arr\),其中连续的相等元素构成的子序列称为平台,设计一个算法求 \(arr\)中最长平台的长度。

int maxLength(int arr[], int n)
{
    int len = 1;
    int max = 1;
    int start = 0;

    for (int i = 1; i < n; i++)
    {
        if (arr[i] == arr[start])
            len++;
        else
        {
            if (len > max)
                max = len;

            start = i;
            len = 1;
        }
    }

    if (len > max)
        max = len;

    return max;
}

设有一个顺序表\(L\), 其元素为整型数据,设计一个算法将\(L\)中所有小于\(0\)的整数放在前半部分,大于等于\(0\)的整数放在后半部分。

void change(SqList &L)
{
    int tmp;
    int i = 0, j = L.length - 1;
    while (i < j)
    {
        while (i < j && L.data[i] < 0)
            i++;
        while (i < j && L.data[j] >= 0)
            j--;
        if (i < j)
        {
            tmp = L.data[i];
            L.data[i] = L.data[j];
            L.data[j] = tmp;
        }
    }
}

设有一个顺序表\(L\),其元素为整型。设计一个尽可能高效的算法,以第一个元素为分界线,将所有小于等于它的元素移到该元素前面,将所有大于它的元素移到该元素的后面。

void move(SqList &L)
{
    int i = 0;
    j = L.length - 1;
    int pivot = L.data[0];
    int tmp;

    while (i < j)
    {
        while (i < j && L.data[j] > pivot)
            j--;
        while (i < j && L.data[i] <= pivot)
            i++;

        tmp = L.data[i];
        L.data[i] = L.data[j];
        L.data[j] = tmp;

    }

    if (L.data[i] < pivot)
    {
        tmp = L.data[0];
        L.data[0] = L.data[i];
        L.data[i] = tmp;
    }
}

void move(SqList &L)
{
    int i = 0, j = L.length - 1;
    int pivot = L.data[0];

    while (i < j)
    {
        while (i < j && L.data[i] > pivot)
            j--;
        L.data[i] = L.data[j];
        i++;
        while (i < j && L.data[i] <= pivot)
            i++;
        L.data[j] = L.data[i];
        j--;
    }

    L.data[i] = pivot;
}


void move(SqList &L)
{
    int i = 0;
    int pivot = L.data[0];
    int tmp;

    for (int j = 1; j < L.length; j++)
    {
        i++;
        if (i != j)
        {
            tmp = L.data[i];
            L.data[i] = L.data[j];
            L.data[j] = tmp;
        }
    }
    tmp = L.data[0];
    L.data[0] = L.data[i];
    L.data[i] = tmp;
}

已知长度为 \(n\) 的线性表 \(L\) 采用顺序存储结构,编写一个时间复杂度为\(O(n)\),空间复杂度为\(O(1)\) 的算法,该算法删除线性表中所有值为\(x\)的数据元素。

void deleteSameElements(SqList &L, int x)
{
    int base = 0;

    for (int i = 0; i < L.length; i++)
    {
        if (L.data[i] != x)
        {
            L.data[base] = L.data[i];
            base++;
        }
    }
    L.length = base;
}

void deleteSameElements(SqList &L, int x)
{
    int count = 0;
    int i = 0;

    while (i < L.length)
    {
        if (L.data[i] == x)
            count++;
        else
            L.data[i - count] = L.data[i];
        i++;
    }
    L.length = L.length - count;
}

已知长度为 \(n\) 的线性表 \(L\) 采用顺序存储结构,试设计一个时间复杂度空间复杂度两方面都尽可能高效的算法,该算法删除线性表中元素值为\([x,y]\)之间的所有数据元素。

单链表

单链表是用任意一组存储单元存放线性表中的元素,每个节点通过一个指针指向后继节点。这组存储单元可以是连续的也可以是不连续的。

  • 通过头节点(带头节点的单链表)或首节点(不带头节点的单链表)的指针来标识一个链表
  • 从一个已知节点出发,只能访问该节点和通过\(next\)指针访问其后继节点,无法直接找到该节点之前的其他节点
  • 在单链表中插入一个节点或者删除一个节点必须已知其前驱节点,插入和删除操作不需要移动节点
typedef struct LinkedNode
{
  T data;  //T表示数据类型
  struct LNode* next;
}LinkedList;

建立单链表的两种方式

  • 头插法「逆序了」

    void creatLinkedList(LinkedList* &L, int a[],int n)
    {
     LinkedList* s;
     L=(LinkedList*)malloc(sizeof(LinkedList));
     L->next=NULL;
    
     for(int i=0;i<n;i++)
     {
       s=(LinkedList*)malloc(sizeof(LinkedList));
       s->data=http://www.mamicode.com/a[i];>
  • 尾插法「还是原来的顺序」

    void creatLinkedList(LinkedList* &L, int a[],int n)
    {
     LinkedList* s,*r;
     L=(LinkedList*)malloc(sizeof(LinkedList));
     r=L; //r始终指向终端节点,开始时指向头结点
    
     for(int i=0;i<n;i++)
     {
       s=(LinkedList*)malloc(sizeof(LinkedList));
       s->data=http://www.mamicode.com/a[i];>

单链表的基本操作

  • 按序号查找节点值算法

    int findNode(LinkedList *L,int i,int &e)
    {
    int j=0;
    LinkedList *ptr=L;
    
    while(ptr!=NULL && j<i)
    {
      j++;
      ptr=ptr->next;
    }
    if(NULL==ptr) return 0;
    else
    {
     e=ptr->data;
     return 1;
    }
    }
    
  • 按元素值查找序号算法

    int findNode(LinkedList *L,int key)
    {
    LinkedList *ptr=L->next;
    int index=0;
    
    while(ptr!=NULL && ptr->data!=key)
    {
      index++;
      ptr=ptr->next;
    }
    
    if(ptr!=NULL) return index;
    else return 0;
    }
    
  • 插入元素:将值为\(x\)的元素的新节点插入到第\(i\)个节点的位置上,即先在单链表中找到插入节点的前驱节点,即第\(i-1\)个节点,再在其后插入新节点。

    s->next=p->next;
    p->next=s;
    
  • 删除元素:将单链表中的第 \(i\) 个节点删除。

    q=p->next;
    p->next=q->next;
    free (q);
    

有一个线性表\((a_1,a_2,...,a_n)\) 采用带头节点的单链表\(L\)存储,设计一个就地算法将其就地逆置,所谓“就地”是指算法的辅助空间为\(O(1)\).

void reverse(LinkedList * & L)
{
  LinkedList *ptr=L->next;
  LinkedList *q;
  L->next=NULL;
  while(ptr!=NULL)
  {
    q=ptr->next;
    ptr->next=L->next;
    L->next=ptr;
    ptr=q;
  }
}

设\(C=\{a_1,b_1,a_2,b_2,...,a_n,b_n\}\) 为一线性表,采用带头节点的\(hc\)单链表存放,设计一个就地算法,将其拆分为两个线性表(它们都是用单链表存放)使得\(A=\{a_1,a_2,...,a_n\},B=\{b_1,b_2,...,b_n\}\)

void split(LinkedList *hc,LinkedList *&ha,LinkedList *&hb)
{
  LinkedList *r=ha;
  LinkedList *p=hc->next;
  LinkedList *q;
  
  hb->next=NULL;
  while(p!=NULL)
  {
    r->next=p;
    r=p;
    
    p=p->next;
    
    q=p->next;
    p->next=hb->next;
    hb->next=p;
    p=q;
  }
  r->next=NULL;  //别忘了
}

王道题讲解

有一个带头结点的单链表\(L\),设计一个算法使其元素递增有序。

void sort(LinkedList * &L)
{
  LinkedList *p=L->next;
  LinkedList *q=p->next;
  LinkedList *pre,tmp;
  
  p->next=NULL:   //含有一个元素的有序单链表
  p=q;
  
  while(p!=NULL)
  {
    q=p->next;
    pre=L;   //单链表只能从头开始往后寻找节点
    
    tmp=pre->next;
    while(tmp!=NULL && tmp.data < p->data)
    {
      pre=tmp;
      tmp=tmp->next;
    }
    
    p->next=pre->next;
    pre->next=p;
    
    p=q;
  }
}

给定两个单链表,编写算法找出其公共的节点
分析:从头到尾扫描单链表\(A\),判断当前元素是否在单链表\(B\)中出现,若在则插入到单链表\(C\)中。

void findSameNode(LinkedList *A,LinkedList *B,LinkedList *&C)
{
  LNode *p=A->next;
  LNode *q=B->next;
  LNode *r;
  
  C=(LNode*)malloc(sizeof(LNode));
  C->next=NULL;
  r=C;
  while(p!=NULL)
  {
    q=B->next;
    while(q!=NULL && q.data!=p.data)
         q=q->next;
    
    if(q!=NULL)
    {
      r->next=p;
      r=p;
    }
    p=p->next;
  }
}

\(2010\)年联考真题
设将\(n\) 个整数存放到一维数组R中。试设计一个在时间和空间尽可能高的算法。将\(R\)中保存的序列循环左移\(p (0 \lt q\lt n)\)个位置,即将\(R\)中的数据由\((X_0,X_1,...,X_{n-1})\) 变换为\((X_p,X_p+1,...,X_{n-1},X_0,X_1,....,X_{p-1})\)

void reverse(int R[],int left,int right)
{
 int i=left,j=right;
 int tmp;
 while(i<j)
 {
   tmp=R[i];
   R[i]=R[j];
   R[j]=tmp;
   i++;
   j--;
 }
}

void leftShift(int R[],int n,int p)
{
  if(p>0 &&p<n)
  {
   reverse(R,0,n-1);
   reverse(R,0,n-p-1);
   reverse(R,n-p,n-1);
  }
}

\(2011\)年联考真题
给定两个数组\(A\)和\(B\),数组的长度为\(n\),两个数组都分别有序,求出两个数组中的所有数的中位数。

int search(int A[],int B[],int n)
{
  int i,j,k;
  i=j=k=0;
  while(i<n &&j<n)
  {
    k++;
    if(A[i]<B[j])
    {
      if(k==n)
        return A[i];
       i++;
    }
    else
    {
      if(k==n)
        return B[j];
      j++;
    }
  }
}

分别求出两个升序序列\(A\),\(B\)的中位数,记为\(a\),\(b\)。若\(a=b\),则\(a\)或\(b\)即为所求,否则舍弃\(a,b\)中较小者所在序列的较小一半,同时舍弃较大者所在序列的较大一半,要求两次舍弃的元素个数相同(每次从左侧和右侧删除相同个数的元素后,新的两个数组,它们的中位数与原始数组的中位数是相同的)。重复上述过程,直到两个序列均只含一个元素为止,则较小的即为所求的中位数

int searchMid(int A[],int B[],int n)
{
  int startA,midA,endA;
  int startA,midB,endB;
  
  startA=0; endA=n-1; 
  startB=0; endB=n-1;
  
  while(startA!=endA || startB!=endB)
  {
     midA=(startA+endA)/2;
     midB=(startB+endB)/2;
     
     if(A[midA]==B[midB])
        return A[midA];
        
     if(A[midA]<B[midB])
     {
       if((startA+endA)%2==0)//若元素个数为奇数时
       {
         startA=midA;  //舍弃A中间点以前的部分且保留中间点
         endB=midB;    //舍弃B中间点以后的部分且保留中间点
       }
       else  //若元素个数为偶数时
       {
        startA=midA+1;  //舍弃A的前半部分,每次舍弃的长度相同,可以保证同时到达
        endB=midB;      //舍弃B的后半部分
       }
     }
     else if(A[midA] > B[midB])
     {
       if((startA+endA)%2==0)//若元素个数为奇数时
       {
         endA=midA;  //舍弃A中间点以后的部分且保留中间点
         startB=midB;//舍弃B中间点以前的部分且保留中间点
       }
       else  //若元素个数为偶数时
       {
        endA=midA;       //舍弃A的后半部分
        startB=midB+1;   //舍弃B的前半部分
       }
     }
  }
  
  return A[startA]<B[startB]?A[startA]:B[startB];
}
  • 若\(A\)和\(B\)数组的长度为\(2k+1\),取\(A\)数组的中位数为\(A[k]\),\(B\)数组的中位数为\(B[k]\),\(A\)和\(B\)组合起来的中位数应该是第\(2k+1\)大的那个数。如果\(A[k]==B[k]\),则\(A[k]\)必定为第\(2k+1\)大的数,是所有数字的中位数。如果\(A[k]\gt B[k]\),则\(A[k]\)至少为第\(2k+2\)大的数,\(B[k]\)至多为第\(2k+1\)大的数,中位数介于\(B[k]\)和\(A[k]\)之间。
  • 若\(A\)和\(B\)数组的长度为\(2k\),按照题目所述条件,则\(A\)的中位数为\(A[k-1]\),则\(B\)的中位数为\(B[k-1]\),\(A\)和\(B\)组合起来的中位数应该是第\(2k\)大的那个数,若\(A[k-1]==B[k-1]\),则\(B[k-1]\)必为第\(2k\)大的那个数,即所有数字的中位数。如果\(A[k-1]\gt B[k-1]\),则\(A[k-1]\)至少为第\(2k\)大的数,\(B[k-1]\)至多为第\(2k-1\)大的数,中位数介于\(B[k-1]\)和\(A[k-1]\)之间。

\(2013\)年联考真题
技术分享?
主元素问题方法一:对数组中元素进行计数,然后查看出现次数最多的元素,若次数大于一半,则为主元素。这种方式只需要对数组扫描一遍,时间复杂度为\(O(n)\),空间复杂度为\(O(n)\)。

int mainElement(int A[],int n)
{
  int *p=(int*)malloc(sizeof(int)*n);
  int max=0;
  
  for(int i=0;i<n;i++) p[i]=0;
  
  for(int i=0;i<n;i++)
  {
    p[A[i]]++;
    if(p[A[i])>p[A[max]]) max=A[i];
  }
  
  if(p[max]>(n/2)  return max;
  else return -1;
}

使用快速排序,然后统计相同元素出现的最大次数

void quickSort(int A[],int left,int right)
{
 int i=left,j=right;
 int tmp;
 
 if(i<j)
 {
    tmp=A[i];
    while(i<j)
    {
      while(i<j && A[j]>tmp) j--;
      A[i]=A[j];
      i++;
      while(i<j && A[i]<=tmp) i++;
      A[j]=A[i];
      j--;
    }
    A[i]=tmp;
    quickSort(A,left,i-1);
    quickSort(A,i+1,right);
 }
}
//然后使用平台算法

数组中存在主元素时,所有的非主元素个数和必少于一半。如果让主元素与一个非主元素"配对“,则最后多出来的元素(没有元素与之配对)就是主元素。从前往后扫描数组元素,假定遇到的当前值选定为主元素,再次遇到它时计数加1,遇到不等的值时,计数减1。当计数减为0后,将遇到的下一个值重新选定为主元素。扫描完毕,当前选定的元素(计数值大于0)可能是主元素,但未必是主元素。还需要对数组再进行一次扫描,记录它出现的实际个数,以判定它是否是主元素。

void mainElemment(int A[],int n)
{
 int base=A[0];
 int count=1;
 int count2=0;
 for(int i=1;i<n;i++)
 {
   if(A[i]==base)
      count++;
   else 
   {  
       if(count>0) count--;
       else 
       {
        base=A[i];
        count=1;
       }
    }
 }
 
 if(count>0)
 {
    count2=0;
    for(int i=0;i<n;i++)
    {
      if(base==A[i])
        count2++;
    }
 }
 if(count2>(n/2))  return base;
 else return -1;
}

线性表