。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。从存储数据集和得角度讲,链表和数组是很相识的。
数组也许是最常见的数据组织形式,在很多编程语言中,他的定义和使用都是相当方便的,如下我们定义一个数组,并初始化前三个元素。
void ArrayTest() {
int scores[100];
scores[0] = 1;
scores[1] = 2;
scores[2] = 3;
}
在内存表示形式上,数组的表示大致如下:
一旦数组建立以后,就很容易通过下标访问他的任意数据元素。访问数组时如:score[2],就是访问的第三个元素。即使如此,数组有以下缺点:
1, 数组的大小(元素个数)是固定的,不可变的;在进行编译时,就会根据数组的定义来分配固定大小的内存空间。
2, 正是由于问题一得存在,我们在声明时,就会尽量的申请更大的空间,但是偶尔我们的空间还是不够的,如:我们要是存score[101]?
3, 要把一个数据插入到数组的开头,是不是相当麻烦?所有的元素向后走一步?
然而链表的产生正是弥补了数组的种种缺陷,相比较数组要分配固定的连续大小的空间,链表的元素在内存空间分配上可以使不连续的。
基础知识:指针,malloc(),free()
链表的组织形式如下图:
一般链表有表头和身体组成,我们在创建链表时。用malloc为链表在heap上分配空间,然后linklist得每一个元素是以node的形式组织的:数据域和指针域。
如果是空链表,那么首指针就是指向NULL,在进行链表操作时,一定要考虑链表是否为空。
链表的节点和节点指针:
节点的定义:
struct node {
int data;//存储数据;
struct node* next;//存储指向下一个节点的指针;
};
下面范例链表的创建:
struct node* BuildOneTwoThree() {
struct node* head = NULL;
struct node* second = NULL;
struct node* third = NULL;
head = malloc(sizeof(struct node)); // allocate 3 nodes in the heap
second = malloc(sizeof(struct node));
third = malloc(sizeof(struct node));
head->data = 1; // setup first node
head->next = second; // note: pointer assignment rule
second->data = 2; // setup second node
second->next = third;
third->data = 3; // setup third link
third->next = NULL;
return head;
}
//上例的等效创建方法:
struct node * addnode(struct node *head,int data)
{
struct node* tmp;
if(head==null)
{
head=(struct node *)malloc(sizeof(struct node));
if(head==null){
printf(“create node failed”);
exit(0);
}
head->data=data;
head->next=head;
}
else {
tmp=head;
while(tmp->next!=head)
tmp=tmp->next;
tmp=(struct node *)malloc(sizeof(struct node));
if(tmp==null)
{
printf(“malloc failed”);
exit(0);
}
else
{
tmp=tmp->next;
tmp->data=data;
tmp->next=head;
}
return head;
}
}
int main(int argc,char *argv[])
{
struct node *head=null;
head=addnode(head,1);
head=addnode(head,2);
head=addnode(head,3);
}
链表的长度:
用函数length()可以很容易的求出链表的元素个数;
int length(struct node *head)
{
struct node *current=head;//函数调用结束,current 自动被回收;
int count=0;
while(current->next!=NULL)
{
current=current->next;
count++;
}
return count;
}
上述函数很简单,但是有两点:
参数传递的是head指针------------------head node ;
与局域指针进行交互;-------------------current node;
指针节点插入到链表的头部:
1, malloc;
//在heap上创建node,然后令其data等于预定值,
newnode=malloc(sizeof(struct node));
newnode->data=”data set”;
2, next转变;
//newnode->next=head;
3, 改变头结点;
head=newnode;
《被调函数如果想改变主调函数的内存内容,很多途径:一,作为函数参数传递,然后返回该参数;地址传递(传递参数的地址,如果参数是指针类型的,那就是“**”型的)》
void Push(struct node** headRef, int data) {
struct node* newNode = malloc(sizeof(struct node));
newNode->data = data;
newNode->next = *headRef; // The '*' 指向了真实的主调函数相关内存;
*headRef = newNode; // ditto
}
void PushTest() {
struct node* head = BuildTwoThree();//Push(&head, 1); // note the &
Push(&head, 13);
}
实现链表的遍历步骤:
1, 定义遍历函数,参数是指向节点指针的指针,如:struct node **;
2, 用&获取要改变节点的地址;主函数中调用遍历函数,参数书写为:&节点指针名;
3, 在被调函数中用*来改变节点指针的值;
下面的函数使头结点指向NULL;
void ChangeToNull(struct node** headRef)
{
*headRef = NULL;
}
void ChangeCaller()
{
struct node* head1;
struct node* head2;
ChangeToNull(&head1);
ChangeToNull(&head2);
}
在创建链表时一般采用头插法,结果是插入的数值将是倒叙的;下面例子实现的是尾插法
struct node* BuildWithSpecialCase() {
struct node* head = NULL;
struct node* tail;
int i;
Push(&head, 1);
tail = head;
for (i=2; i<6; i++) {
Push(&(tail->next), i);
tail = tail->next;
}
return(head); // head == {1, 2, 3, 4, 5};
}
在进行插入时,考虑到第一个元素采用头插法,而剩余的采用尾插法,简单的方法是假设在链表在插入前已经有一个虚假节点,那么就可以统一采用尾插法进行链表的建立;
struct node* BuildWithDummyNode() {
struct node dummy;
struct node* tail = &dummy;
int i;
dummy.next = NULL;
for (i=1; i<6; i++) {
Push(&(tail->next), i);
tail = tail->next;
}
return(dummy.next);
}
上述的虚假节点采用临时的处理方法,平时多用永久性的虚假节点;也有另外一种方法就是局部指针变量的方法:局部指针变量指向的链表的最后一个指针而不是最后一个节点;
struct node* BuildWithLocalRef() {
struct node* head = NULL;
struct node** lastPtrRef= &head;
int i;
for (i=1; i<6; i++) {
Push(lastPtrRef, i);
lastPtrRef= &((*lastPtrRef)->next); }
return(head);
}
上面的函数有点危险。。。。。。。。。。。。。。。。。。。。。。。